13

enter image description hereI am trying to make a page that allows user after clicking on a word to get a small suggestion box (like a relly small popup) where he can click and choose synonyms that he wants.

I am not sure in what language this could be done probably javascript but I didn't find any examples for that.

The html code is bellow:

Original:
I <b class="synonyms" style="color:black;" 
title="love|really like|really love">like</b> apples.

The result should be(after a user chooses synonyms):
I <b>{like|love}</b> apples.

So for examples when he click on "like" from the sentence "I like apples" there should be a small suggestion box where he could choose among all the suggested options (love|really like|really love).

In the results is original plus what he chooses.

This is an example i javascript, however I am not sure if there is a way to click on specific word (there could be more than one word in a sentence), and also is there way to style the suggestion box and add list of words to choose from by clicking on them.

<!DOCTYPE html>
<html>
<body>

<p>I <b id="demo">like</b> apples.</p>

<button onclick="choose()">Try it</button>


<script>
function choose() {
    var synonym = prompt("Choose synonyms:", "like");
    
    if (synonym != null) {
        document.getElementById("demo").innerHTML =
        "{" + "like" + "|" + synonym + "}";
    }
}
</script>

</body>
</html>
Brana
  • 1,197
  • 3
  • 17
  • 38

5 Answers5

9

...however I am not sure if there is a way to click on specific word (there could be more than one word in a sentence), and also is there way to style the suggestion box and add list of words to choose from by clicking on them.

Break down your problem into steps. This will make it easy for you to understand the problem domain and design a solution. From what I could make out of your question, the broad steps and their implementation could be like the ones described below.

Note 1: The answer is based on pure JavaScript. Remember that all frameworks like jQuery etc are JavaScript only abstracted at a higher level. It is important for you to learn basic JavaScript first.

Note 2: I've provided references for key concepts (for you to get more information and learn from) in the form of embedded links throughout this answer.

1) Markup and Javascript setup for words: There are certain words which have synonyms. The synonyms are available in the markup itself in the title attribute. The markup you arrived at, is just fine:

Markup:

<p>
    I <i class="synonyms" title="love|really like|really love">like</i> apples. 
    I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. 
</p>

We need to be able to identify all words that carry synonyms, so that we could use Javascript to manipulate those. These are identified in the markup as i elements with a class called synonyms.

Javascript:

var words = [].slice.call(document.querySelectorAll('i.synonyms'));

querySelectorAll returns a node list, so easiest way to convert that to an array is to call slice on array prototype. We need an array so that we can iterate it later on.

2) Markup and Javascript setup for menu: There needs to be a suggestion box to be popped up. So, just add an element which will hold your synonyms. You already know that there will be a list of synonyms, so semantically it makes sense to have a list element. And give it an id. We will fill it dynamically later on.

Markup:

<ul id="synonymMenu"></ul>

Javascript:

var menu = document.getElementById('synonymMenu');

3) Suggestion box menu should popup: whenever such a word is clicked. So, we need to add and event listener on all such words which will listen to a click event. We already have the words in the variable words in step one above. We just iterate and add the event listener to execute the function manageMenu. We will define that function later. While we are at it, we also cache the existing word in a data attribute to be able to use it later, using setAttribute.

words.forEach(function(wrd) {
    wrd.setAttribute('data-word', wrd.textContent);
    wrd.addEventListener('click', manageMenu);
});

4) Replace word with selected synonym: whenever, a synonym is clicked in the suggestion box menu. So we have to add a click event listener to the synonym list as well. We already have the menu stored in the variable menu in step 2 above. Just add the listener to execute function applySynonym. We will define that function later.

menu.addEventListener('click', applySynonym);

5) We have to close dangling suggestion box as well: We could do that by clicking anywhere on the body. Just add another click event handler on body. Execute function toggleMenu with a hide parameter. Will define this function later.

document.body.addEventListener('click', function() {
    toggleMenu('hide');
});

6) Create a list of synonyms from the title attribute and show: it in the suggestion box menu, when the word is clicked. That we will define in the manageMenu function which we declared in step 3. Explanation is in code comments.

function manageMenu(e) {
    // define variables
    var synonyms, optn, link, position;  

    // clear existing list and then show the menu
    clearMenu(); toggleMenu('show'); 

    // cache the click event target to a variable to be used later
    currentWord = e.target;

    // get the position of word relative to viewport
    position = currentWord.getBoundingClientRect();

    // use that position to shift the popup menu near to the word
    menu.style.top = position.top + 24 + 'px';
    menu.style.left = position.left + 2 + 'px';

    // extract title attribute, split by | and store in array
    synonyms = currentWord.getAttribute('title').split('|');

    // iterate array creating an li and anchor for each synonym
    // createElement creates a new element
    // appendChild adds an element to another
    synonyms.forEach(function(syn) {
        optn = document.createElement('li');
        link = document.createElement('a');
        link.setAttribute('href', '#'); link.textContent = syn;
        // add anchor to li, and the li to the menu
        optn.appendChild(link); menu.appendChild(optn);
    });
    // stop propagation of click, so that it doesn't go to body
    e.stopPropagation(); 
}

The key references for you in the code above are about using the event object and its target, getting the position of word relative to viewport, createElement, appendChild, and stopPropagation

7) Synonym should be appended to the original word: and shown in its place, once a synonym is clicked. This we will define in the applySynonym fucntion that we referenced in step 4.

function applySynonym(e) {
    var txt = '';

    // Because we added event listener to the parent ul element, 
    // we have to check if the clicked element is the anchor or not
    if (e.target.tagName != 'A') { return false; }

    // We retrieve the orginal text from the data attribute, 
    // which we cached in step 6 above. And append current anchor's text
    txt += '{' + currentWord.getAttribute('data-word') + '|';
    txt += e.target.textContent + '}';
    // replace the text of the word
    currentWord.textContent = txt;
    toggleMenu('hide'); // hide the suggestion box menu
    // stop propagation of click, so that it doesn't go to body
    // prevent default so that clicking anchor doesn't jump to top
    e.stopPropagation(); e.preventDefault();
}

The key references for you in the code above are about preventDefault.

8) We define the rest of helper functions:

function toggleMenu(mode) {
    if (mode == 'show') { menu.style.display = 'block'; }
    if (mode == 'hide') { menu.style.display = 'none'; }
}

function clearMenu() {
    // we loop the child nodes of menu ul element, 
    // remove the last child (last li) of that ul element, 
    // until it does not has-child-nodes.
    while (menu.hasChildNodes()) { 
        menu.removeChild(menu.lastChild); 
    }
}

The key references for you in the code above are about hasChildNodes, removeChild, and lastChild.

9) Define the presentation via CSS, especially making the menu positioned absolutely, hiding it on first load and also beautify the presentation:

ul#synonymMenu {
    position: absolute; display: none;
    ...
    border: 1px solid #bbb; background-color: #efefef;
}

10) Test.

Demo Fiddle: https://jsfiddle.net/abhitalks/zske2aoh/

Demo Snippet:

(function() {
 var menu = document.getElementById('synonymMenu'), 
  words = [].slice.call(document.querySelectorAll('i.synonyms')), 
  currentWord = null
 ;
 
 words.forEach(function(wrd) {
  wrd.setAttribute('data-word', wrd.textContent);
  wrd.addEventListener('click', manageMenu);
 });
 menu.addEventListener('click', applySynonym);
 document.body.addEventListener('click', function() {
  toggleMenu('hide');
 });

 function manageMenu(e) {
  var synonyms, optn, link, position; 
  clearMenu(); toggleMenu('show'); 
  currentWord = e.target;
  position = currentWord.getBoundingClientRect();
  menu.style.top = position.top + 24 + 'px';
  menu.style.left = position.left + 2 + 'px';
  synonyms = currentWord.getAttribute('title').split('|');
  synonyms.forEach(function(syn) {
   optn = document.createElement('li');
   link = document.createElement('a');
   link.setAttribute('href', '#'); link.textContent = syn;
   optn.appendChild(link); menu.appendChild(optn);
  });
  e.stopPropagation();
 }
 
 function applySynonym(e) {
  var txt = '';
  if (e.target.tagName != 'A') { return false; }
  txt += '{' + currentWord.getAttribute('data-word') + '|';
  txt += e.target.textContent + '}';
  currentWord.textContent = txt;
  toggleMenu('hide');
  e.stopPropagation(); e.preventDefault();
 }
 
 function toggleMenu(mode) {
  if (mode == 'show') { menu.style.display = 'block'; }
  if (mode == 'hide') { menu.style.display = 'none'; }
 }
 
 function clearMenu() {
  while (menu.hasChildNodes()) { 
   menu.removeChild(menu.lastChild); 
  }
 }
 
})();
* { font-family: sans-serif; }
html, body { height: 100%; }
i.synonyms { cursor: pointer; color: #333; }
ul#synonymMenu {
 position: absolute; display: none;
 width: auto; max-height: 120px; 
 overflow: hidden; overflow-y: auto;
 list-style: none; padding: 0; margin: 0; 
 border: 1px solid #bbb; background-color: #efefef;
 box-shadow: 0px 0px 6px 1px rgba(128,128,128,0.3);
}
ul#synonymMenu > li { display: block; }
ul#synonymMenu a { 
 display: block; padding: 4px 20px 4px 6px; 
 color: #333; font-size: 0.9em; text-decoration: none;
}
ul#synonymMenu a:hover {
 background-color: #99b;
}
<p>
I <i class="synonyms" title="love|really like|really love">like</i> apples. 
I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. 
</p>
<ul id="synonymMenu"></ul>

Edit:

Per the Op's comments the code has been updated to accommodate multiple selections of synonyms via checkboxes. Added complexity is around adding checkboxes instead of plain anchors, changing the event listeners for the same, updated styles, and the logic to retain pre-existing selection on repeat clicks.

Updated Fiddle: https://jsfiddle.net/abhitalks/ffpL4f7k/

Updated Snippet:

(function() {
 var menu = document.getElementById('synonymMenu'), 
  menuWrap = document.getElementById('menuWrapper'),
  okButton = document.getElementById('synOk'), 
  words = [].slice.call(document.querySelectorAll('i.synonyms')), 
  currentWord = null
 ;
 
 words.forEach(function(wrd) {
  wrd.setAttribute('data-word', wrd.textContent);
  wrd.addEventListener('click', manageMenu);
 });
 okButton.addEventListener('click', applySynonym);
 document.body.addEventListener('click', function(e) { 
  if (isDescendant(menuWrapper, e.target)) {
   return;
  }
  toggleMenu('hide');
 });

 function manageMenu(e) {
  var synonyms, opt, lbl, chk, txt, position, existing; 
  clearMenu(); toggleMenu('show'); 
  currentWord = e.target;
  position = currentWord.getBoundingClientRect();
  menuWrap.style.top = position.top + 20 + 'px';
  menuWrap.style.left = position.left + 2 + 'px';
  existing = currentWord.textContent;
  synonyms = currentWord.getAttribute('title').split('|');
  synonyms.forEach(function(syn) {
   opt = document.createElement('li'); 
   lbl = document.createElement('label');
   chk = document.createElement('input'); 
   chk.setAttribute('type', 'checkbox'); 
   txt = document.createTextNode(syn);
   lbl.appendChild(chk); 
   lbl.appendChild(txt); 
   opt.appendChild(lbl); 
   menu.appendChild(opt);
  });
  preSelect(existing);
  e.stopPropagation();
 }
 
 function preSelect(existing) {
  var labels = [].slice.call(menu.querySelectorAll('label'));
  labels.forEach(function(lbl) {
   if (existing.indexOf(lbl.textContent) > -1) {
    lbl.firstChild.checked = true;
   }
  });
 }
 
 function applySynonym(e) {
  var txt = '', labels, checked, selected;
  labels = [].slice.call(menu.querySelectorAll('label'));
  checked = labels.filter(function(lbl){
   return lbl.firstChild.checked;
  });
  selected = checked.map(function(lbl){
   return lbl.textContent;
  }).join('|');
  
  txt += '{' + currentWord.getAttribute('data-word') + '|';
  txt += selected + '}';
  currentWord.textContent = txt;
  toggleMenu('hide');
  e.stopPropagation(); 
 }
 
 function toggleMenu(mode) {
  if (mode == 'show') { menuWrap.style.display = 'block'; }
  if (mode == 'hide') { menuWrap.style.display = 'none'; }
 }
 
 function clearMenu() {
  while (menu.hasChildNodes()) { 
   menu.removeChild(menu.lastChild); 
  }
 }
 
 function isDescendant(parent, child) {
   var node = child.parentNode;
   while (node != null) {
    if (node == parent) {
     return true;
    }
    node = node.parentNode;
   }
   return false;
 }

})();
* { font-family: sans-serif; box-sizing: border-box; }
html, body { height: 100%; }
div.wrap { 
 border: 1px solid #ddd; max-height: 480px; 
 padding: 4px 22px 4px 4px; font-size: 0.9em;
 overflow: hidden; overflow-y: auto;
}
i.synonyms { cursor: pointer; color: #333; }
div#menuWrapper {
 position: absolute; display: none; width: 128px; 
 padding: 4px; margin: 0; 
 border: 1px solid #bbb; background-color: #efefef;
 box-shadow: 0px 0px 6px 1px rgba(128,128,128,0.3);
}
ul#synonymMenu {
 max-height: 120px; 
 overflow: hidden; overflow-y: auto;
 list-style: none; padding: 0; margin: 0; 
}
ul#synonymMenu > li { display: block; }
ul#synonymMenu label { 
 display: block; color: #333; font-size: 0.9em; 
 padding: 2px 18px 2px 4px; 
}
ul#synonymMenu label:hover { background-color: #99b; }
button#synOk { padding: 2px; width: 100%; }
<div class="wrap">
 <p>
 I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. 
 </p>
 <p>
 I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. 
 </p>
 <p>
 I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. I <i class="synonyms" title="love|relish|savor">like</i> apples. I <i class="synonyms" title="love|relish|savor|enjoy|patronize|adore">like</i> oranges. 
 </p>
</div>
<div id="menuWrapper">
 <ul id="synonymMenu"></ul>
 <hr/>
 <button id="synOk">Ok</button>
</div>

Abhitalks
  • 27,721
  • 5
  • 58
  • 81
  • Hello, this is an amazing solution. I especially like that you have explained eveything in detail So I can learn a lot from it. I have just 2 small problems that need to be solved. Here is updated versiobn of your code where I put the content in a div box - https://jsfiddle.net/zske2aoh/4/. It works fine there. However on my page it the suggestion box is about 500px above and 100ox right the word I clicked at. – Brana Mar 21 '16 at 16:07
  • 1
    @Brana: I seem to be unable to understand what you are trying to say here. I can't see the updated version you are talking about and so can't figure out what content you want to put in table. – Abhitalks Mar 21 '16 at 16:12
  • Also it is crucial that I have the ability to select multiple synonyms from the list. Example: I {like|love|really like} apples. Here I can create just one synonym. Is this possible using this approach? Sorry I didn't said this before, I just used the examples I used for simplicity :) – Brana Mar 21 '16 at 16:14
  • Hello, I added a picture - the suggestion box is far above the content. I tried to chnage css but still the I couldn't move the suggestion box on the place it should be. – Brana Mar 21 '16 at 16:33
  • Also is it possible to select multiple synonyms? – Brana Mar 21 '16 at 16:33
  • 1
    Hold on @Brana, Give me a few minutes. – Abhitalks Mar 21 '16 at 16:35
  • 1
    @Brana: Here - https://jsfiddle.net/abhitalks/ffpL4f7k/. It is now grown a little too complicated. Would suggest you take your time and understand it slowly. There are too many things going on. – Abhitalks Mar 21 '16 at 17:35
  • After a while got it to work properly. The css of my site which sometimes used position:relative and your script was inside it so - and of course this caused problem for obvious reasons :) – Brana Mar 22 '16 at 00:16
  • The problem from the picture is caused because you didn't handle the scrooling case - so you should change this like in the answer for others : menuWrap.style.top = position.top + window.pageYOffset + 20 + 'px'; menuWrap.style.left = position.left + window.pageXOffset + 2 + 'px'; – Brana Mar 22 '16 at 00:18
  • 1
    However you did 99.99% and this is an amazing job, pretty complex but you handled it with easy. Thanks very much. – Brana Mar 22 '16 at 00:19
  • 1
    Thank you @brana. It feels good that you were able to find it useful. Yes, I couldn't work out on smaller details like scrolling and positioning etc, due to paucity of time. But it is heartening to know that you could work that out. – Abhitalks Mar 22 '16 at 02:57
  • Why it's implemented not as plugin? – Medet Tleukabiluly Mar 23 '16 at 15:13
  • @MedetTleukabiluly, because one the Op didn't ask for it, and two the kind of implementation is not as important here as learning the algorithmic concepts. – Abhitalks Mar 23 '16 at 15:15
  • @Abhitalks. That is because you coded it well step by stepa and explained everything so I could easily find and change thing I wanted to change. – Brana Mar 23 '16 at 21:27
5

I build the following jQuery component that suits your needs, I believe.
Here's the jsbin if you prefer that.

//jquery component
$.fn.synonyms = function(options){
  options = $.extend({}, {separator: '|'}, options);
  this.each(function(elKey, el){
    var $el = $(el),
        originalText = $el.text(),
        originalTextSpan = $('<span>'+originalText+'</span>');
    $el.html(originalTextSpan);
    var suggestionBox = '<div>';
    $.each($el.attr('data-synonyms').split(options.separator),
           function(key, suggestion){
      suggestionBox+='<span>'+suggestion+'</span> - ';
    }
          );
    suggestionBox = suggestionBox.slice(0, -2);
    suggestionBox += '</div>';
    suggestionBox = $(suggestionBox);
    suggestionBox.css({
      display: 'none'
    });
  
    $el.click(function(){
      suggestionBox.toggle();
    });
  
    suggestionBox.on('click','span',function(){
      var selectedText = $(this).text();
      originalTextSpan.text('{'+originalText+'|'+selectedText+'}');
      onSelected(selectedText);
    });
  
    $el.append(suggestionBox);
  });
  
  
  function onSelected(selectedText){
    if(options.onSelected){
      options.onSelected(selectedText);
    }
  }
};


// How to use the component
$(function(){
  $('[data-synonyms]').synonyms({
    onSelected: function(selectedText){
      alert('you selected:'+selectedText);
    }
  });
});
div[data-synonyms]{
  display: inline;
  position: relative;
  cursor: pointer;
  text-decoration: underline;
}
div[data-synonyms] > div{
  white-space: nowrap;
  position: absolute;
  top: 1.2em;
  left: 0;
  background: #fff;
  border: 1px solid #bbb;
  padding: 2px;
}
div[data-synonyms] > div > span{
  text-decoration: underline;
  cursor: pointer;
}
<!DOCTYPE html>
<html>
<head>
<script src="https://code.jquery.com/jquery-2.1.4.js"></script>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
  I <div data-synonyms="love|really like|really love">like</div> apples. Carrots are the <div data-synonyms="worst|orangest">best</div> though.
</body>
</html>
Swimburger
  • 6,681
  • 6
  • 36
  • 63
  • 2
    You could let the originalText be the first option of the synonyms, so you don't have to show the brackets and the originalText next to the new selection (better/prettier UI I think). Great widget/component btw! – Philip Mar 21 '16 at 02:52
  • @Philip Thanks! I used the {xxx|yyy} formatting because it seemed what he wanted. But yes, I think the text should just be replaced. Maybe the original text should become one of the options after it's being replaced so it can be reverted. – Swimburger Mar 21 '16 at 02:55
  • @Philip, that looks great. I just updated my answer because it didn't work great when there are multiple synonyms on a page. It handles multiple synonym instances better now. – Swimburger Mar 21 '16 at 13:26
4

$(document).ready(function() {
  $("b").on("click", function() {
    var $b = $(this),
      alternatives = this.title.split('|').join('</option><option>');
    $b.after('<select class="selector"><option>&nbsp;</option><option>' + alternatives + '</option><select>');
  });
  $("body").on("change", "select.selector", function() {
    var $sl = $(this),
      txt = $sl.val();
    $sl.prev('b').text(txt);
    $sl.remove();
  });
});
b {
  color: red;
  cursor: pointer;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div class="text">
  <p>I <b title="like|love|adore">like</b> apples, <b title="especially|particularily">particularily</b> if they are <b title="fresh|ripe">ripe</b>.</p>

</div>
Majid Fouladpour
  • 29,356
  • 21
  • 76
  • 127
  • 1
    This is good, however there are a few problems: clicking on a word twice give several dropdown menus, and only the first one works. – thepiercingarrow Mar 21 '16 at 02:13
  • @MarkWright in what browser/platform? The select will self destroy upon selecting an item, so there should not be multiple selects per word, unless you click a word again before making a selection; even then, if you make a selection it is removed and then the next one becomes functional. – Majid Fouladpour Mar 21 '16 at 02:26
  • Ah, now the second and third work. As for the multiple selects - that was because I was clicking then twice before selecting. But thank you, now it works :) – thepiercingarrow Mar 21 '16 at 02:34
1

I created another solution using bootstrap's dropdown:

http://www.bootply.com/rWfTgSwf1z

The idea is to use dropdown for all the words where you want to use synonyms. The current code adds the dropdown manually for each word and also replaces the original word upon selection.

You can have the sentences as following where the synonyms for the words are defined in the data-synonyms attribute:

<div>
  I
  <span data-synonyms="love|really like|really love">like</span>
  apples and
  <span data-synonyms="mangoes|bananas|other fruits">oranges</span>.
</div>

Then, in the javascript we create the dropdowns and replaces the existing elements:

$('[data-synonyms]').each(function () {

  // get the current element
  var $this = $(this);

  // create a dropdown wrapper
  var $dropdownDiv = $('<div>').addClass('dropdown word-dropdown');

  // create the dropdown trigger
  var $a = $('<a>').attr('data-toggle', 'dropdown').text($this.text()).appendTo($dropdownDiv);

  // create the dropdown list
  var $ul = $('<ul>').addClass('dropdown-menu').appendTo($dropdownDiv);

  // get the synonyms and append the existing word
  var synonyms = $this.attr('data-synonyms').split('|');
  synonyms.splice(0, 0, $this.text());

  // create an entry in the dropdown for each synonym
  $.each(synonyms, function (idx, syn) {
    var $li = $('<li>').addClass('synonyms').appendTo($ul).append($('<a>').text(syn.trim()));

    // add a handler which replaces the existing word with the synonym
    $li.on('click', function () {
        $a.text(syn.trim());
    });
  });

  // replace the current element with the dropdown element
  $this.replaceWith($dropdownDiv);

  // activate the dropdown
  $a.dropdown();

});
AKS
  • 18,983
  • 3
  • 43
  • 54
1

Here's a Fiddle

I've used jQuery, and altered the syntax a bit. Every option should be a span with class selectable. It then needs an options attribute. The options are presented the way you suggested.

What my script does differently from the others, is that it doesn't list the option that has already been selected and is usable with the keyboard (try using Tab, Enter and the arrow keys).

$(function () {
  $('.selectable').each(function () {
    var $this = $(this)
    var list  = $this.attr('options').split('|')
    var text  = $this.text()
    $this
      .data('original', text)
      .html('<div><span>' + text + '</span><ul><li tabindex="0">' + list.join('</li><li tabindex="0">') + '</li></ul></div>')
  }).on('mousedown', function (e) {
    e.preventDefault()
    var $this   = $(this)
    var $target = $(e.target)
    var $focus  = $this.find(':focus')
    if ($focus.length) $focus.blur()
    else $this.find('li:not(.active)').eq(0).focus()
    if ($target.is('li')) changeSelection($this, $target)
  }).on('keydown', function (e) {
    var which = e.which
    if (which === 13) changeSelection($(this))
    else if (which === 40) $(this).find(':focus').next().focus()
    else if (which === 38) $(this).find(':focus').prev().focus()
  })
  function changeSelection ($this, $target) {
    $target = $target || $this.find(':focus')
    $target.blur()
    $this
      .one('transitionend', function () {
        $target.addClass('active').siblings().removeClass('active').last().focus().blur()
      })
      .addClass('filled')
      .find('span').text($this.data('original') + ' | ' + $target.text())
  }
})
body {
  font-family: sans-serif;
}

.selectable {
  cursor: default;
}

.selectable:focus {
  outline: none;
}

.selectable div {
  position: relative;
  display: inline-block;
}

.selectable::before,
.selectable::after,
.selectable span {
  color: #F00;
}

.selectable.filled::before {
  content: '{';
  margin-right: .2em;
}

.selectable.filled::after {
  content: '}';
  margin-left: .2em;
}

.selectable ul {
  position: absolute;
  top: calc(100% + .2em);
  left: calc(-.4em - 1px);
  list-style: none;
  margin: 0;
  padding: 1px 0 0;
  overflow: hidden;
  background-color: #DDD;
  z-index: 1;
}

.selectable ul:not(:focus-within) {
  pointer-events: none;
  user-select: none;
  opacity: 0;
  transform: translateY(-5px);
  transition: opacity .25s ease, transform .4s ease;
}

.selectable ul:focus-within {
  transition: opacity .25s ease, transform .4s ease -.15s;
}

.selectable li {
  white-space: nowrap;
  padding: .4em;
  margin: 0 1px 1px;
  background-color: #FFF;
}

.selectable li:hover {
  background-color: #F7F7F7;
}

.selectable li.active {
  display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<p>I <span class="selectable" options="love|really like|really love">like</span> apples.</p>
<p>There's nothing <span class="selectable" options="in this world|in the universe">on earth</span> I eat more than apples.</p>

Hope this helps

Gust van de Wal
  • 5,211
  • 1
  • 24
  • 48