0

Let's say I have 4 items within a paragraph at the top of the page, that when clicked should open up a "more info" div (say, a popup created by adding "active" class).

But, these divs are not near their trigger, so prev/next/sibling manipulations won't work. Is there a way to trigger them some other way, without writing individual event listeners for each pair? Like some loop way? I'm having trouble getting it to work b/c of closures or scope or something.

<section> 
  <ul>
    <li>click question mark to learn more: <span class="questionmark">?</span></li>
    <li>click question mark to learn more: <span class="questionmark">?</span></li>
    <li>click question mark to learn more: <span class="questionmark">?</span></li>
  </ul>
  <div class="tooltip">
    <div class="tooltip-inner">
      <a class="close-tip">&times;</a>
      1</div>
  </div>
  <div class="tooltip">
    <div class="tooltip-inner">
      <a class="close-tip">&times;</a>
      2</div>
  </div>
  <div class="tooltip">
    <div class="tooltip-inner">
      <a class="close-tip">&times;</a>
      3</div>
  </div>  
</section>

and the JS:

var qmks = document.getElementsByClassName('questionmark');
var tooltips = document.getElementsByClassName('tooltip');
var closetips = document.getElementsByClassName('close-tip');

// toggle "active" class on matching tooltip
function openTip(){
  tooltips[i].classList.toggle('active');
  console.log('works ' + i); // test to see if it's triggering, and what it thinks "i" is.
}

// failed attempt to match nth question with nth tooltip. 
for (let i = 0; i < qmks.length; i++) {
  qmks[i].addEventListener('click', openTip);
}

for (let k = 0; k < closetips.length; k++) {
  closetips[k].addEventListener('click', closeTip);
}

function closeTip(){
  for (let j = 0; j<tooltips.length; j++) {
    tooltips[j].classList.remove("active");
  }
}

pen: https://codepen.io/anon/pen/WXgMRG

sashaikevich
  • 167
  • 13
  • You never pass `i` to `openTip`. How should `openTip` know, what `i` is in `tooltips[i]`? – Syntac Nov 28 '17 at 00:34
  • That's the same problem I've identified. When invoked, "i" will always be the global "i". And replacing openTip in the for loop with an anonymous function just doesn't seem right, as it only works b/c I'm using "let" instead of "var". Got a solution? – sashaikevich Nov 28 '17 at 00:49
  • Why are you unhappy with using an anonymous function with `let`? If you don't like doing that, you can use `data-*` attributes, as ztadic91 suggested. But then you have to maintain your references in HTML code, which has it's pros and cons. – Syntac Nov 28 '17 at 00:58
  • I'm unhappy due to my lack of knowledge mostly. I don't know 1) why it works with let and doesn't with var (ie it's witchcraft) and 2) for organization - I want to have the openTip function outside the event listener binding to edit it better. – sashaikevich Nov 28 '17 at 01:03
  • I've commented below to clarify, why closures work. I could make a try to explain the difference between `let` and `var`, but I hope this post https://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example will do a good job at it. If not, let me know! – Syntac Nov 28 '17 at 01:25
  • Yup, that's the one! It was there that I saw the wrapping with the anon function and the it was @Ben McCormick from whom I got the "let" solution. Still reading through all the explanations :-) Wealth of knowledge there... – sashaikevich Nov 28 '17 at 02:06

2 Answers2

0

you could add some dataset attributes to the <li> and to the the .tooltip elements.

And then init a generic listener which would toggle only the selected tooltip.

 <li class="toggler" data-id=1>click question mark to learn more: <span class="questionmark">?</span></li>

and for the tooltip class

<div class="tooltip" data-id=1>
    <div class="tooltip-inner">
      <a class="close-tip">&times;</a>
      3</div>
  </div>  

and add a listener

$('.toggler').on('click', function() {
   var id = this.dataset.id;
   var tooltip = document.querySelector('.tooltip[data-id=' + id']');
   tooltip.classList.toggle('active');
})
ztadic91
  • 2,774
  • 1
  • 15
  • 21
  • Thank you. This will be my fallback solution. Still new to JS, I wanted to know if it was still possible to do it with loops... maybe by extracting the position from htmlcollection of the question mark clicked, and using that index on the tooltip collection or something. – sashaikevich Nov 28 '17 at 00:50
0

The problem is, that your openTip doesn't know i. Use a closure to pass it as an parameter (only js changed):

var qmks = document.getElementsByClassName('questionmark');
var tooltips = document.getElementsByClassName('tooltip');
var closetips = document.getElementsByClassName('close-tip');

// toggle "active" class on matching tooltip
function openTip(i){
  tooltips[i].classList.toggle('active');
  console.log('works ' + i); // test to see if it's triggering, and what it thinks "i" is.
}

// failed attempt to match nth question with nth tooltip. 
for (let i = 0; i < qmks.length; i++) {
  qmks[i].addEventListener('click', function(){openTip(i);});
}

for (let k = 0; k < closetips.length; k++) {
  closetips[k].addEventListener('click', function(){closeTip(k);});
}

function closeTip(i){
  tooltips[i].classList.remove("active");
}
body {
  background:aqua;
}

section {
  width:300px;
  margin:30px auto;
  padding:20px;
  background:#fff;
}


.tooltip {
  position:fixed;
  left:0;
  top:0;
  height:100%;
  width:100%;
  overflow:auto;
  background-color:rgba(0,0,0,0.5);
  z-index:150;
  display:none;
  animation-name:poptool;
  animation-duration:1s;
}
.tooltip.active {
  display:block;
}

.tooltip-inner {
  z-index:160;
  background:#fff;
  padding:20px;
  display:block;
  margin:30px auto;
  width:70%;
  height:500px;
  top:50%;
  border-radius:3px;
  box-shadow: 0 5px 8px 0 rgba(0,0,0,0.2), 0 7px 20px 0 rgba(0,0,0,0.17);
}

.close-tip {
  float:right;
  font-size:40px;
  color:#ddd;
  line-height:1;
   cursor:pointer;
  transition:color 200ms;
  text-decoration:none;
}
.close-tip:hover, .close-tip:focus {
  color:#333;
}

@keyframes poptool{
  from{opacity:0}
  to{opacity:1}
}


@media screen and (max-width:500px){
  .tooltip-inner {
    width:90%;
  }
}
<section> 
  <ul>
    <li>click question mark to learn more: <span class="questionmark">?</span></li>
    <li>click question mark to learn more: <span class="questionmark">?</span></li>
    <li>click question mark to learn more: <span class="questionmark">?</span></li>
  </ul>
  <div class="tooltip">
    <div class="tooltip-inner">
      <a class="close-tip">&times;</a>
      1</div>
  </div>
  <div class="tooltip">
    <div class="tooltip-inner">
      <a class="close-tip">&times;</a>
      2</div>
  </div>
  <div class="tooltip">
    <div class="tooltip-inner">
      <a class="close-tip">&times;</a>
      3</div>
  </div>  
</section>
Syntac
  • 1,687
  • 11
  • 10
  • I see you wrapped it in an anonymous function. I've seen this technique (somewhere) but i don't understand it. Why does it work / what does wrapping it do? What's the reasoning behind it solving the problem? – sashaikevich Nov 28 '17 at 01:00
  • @sashaikevich The reason really is, that `addEventListener` doesn't allow you to specify additional parameters besides the `EventObject` if it did (e.g. like `setTimout` allows to pass params), we could just pass `i` as one of them. Since it doesn't we have to somehow let the function given as Event-Handler already know the value of `i`. A solution for that is to include `i` in the functions definition. Since it is defined inside the scope of the current `let i`, it will reference it respectively. By that however, we really are defining a function for every value of `i` there is. – Syntac Nov 28 '17 at 01:11
  • @sashaikevich So we have many functions in reality. To keep there footprint (e.g. memory, code redundancy) small, instead of defining all of `openTip` in there, as an anonymous function (which would work as well), we only wrap it, so our wrapper only serves to provide access to `i` (which, again, it has, because it was defined in the scope of `let i`), and we don't actually copy any functionality of `openTip`. Hope that answers your question. If you are still confused, let me know. – Syntac Nov 28 '17 at 01:17
  • I am still confused. But I think I just need to learn the fundamentals better to bridge the gap in my understanding. ATM, here's what's got me stumped: - The dom object has property for onclick. It's empty by default. But we want something to happen when it's clicked, so we add our own function - openTip. - But we can't pass anything other than the event as an argument. - to get around this, we wrap openTip in a function. But how does that help? Once clicked the anonFunc will be invoked instead of openT. But it just invokes the same openT. It doesn't add any new parameters. 1/2 – sashaikevich Nov 28 '17 at 02:02
  • 2/2 Anyway, I have the answer to my question AND already an explanation that you kindly provided. So asking more of you is beyond my good conscious. I will brush up on theory, and will re-read your reply once I know more. If I'm still stumped perhaps you'll reply to a PM? Thank you very much for your help @Syntac ! – sashaikevich Nov 28 '17 at 02:04
  • @sashaikevich Good idea! It's indeed important to have a very solid understanding of the basic principles. Anyways, the difference between `openTip` and the anonymous function is the scope they were defined in. Since the anonymous function was defined in the scope of `let i`, it has access to it, within it's definition. After the `for`-loop is completed the scope persists within the function. The garbage-collector won't delete `i`, cause it is still in use by the function. The anonymous function therefore basically helps us to 'remember' `i` and it passes that knowledge to `openTip`. – Syntac Nov 29 '17 at 09:05
  • Got it. Thanks again – sashaikevich Nov 29 '17 at 11:56