I'm using Tooltipster to show a list of items that the user can click so as to enter the item into a textarea. When a tooltip is created, I get its list of items with selectors = $("ul.alternates > li");
However, each time a tooltip is opened the item clicked will be inserted a corresponding number of times; for example if I've opened a tooltip 5 times then the item clicked will be inserted 5 times. I've tried deleting the variable's value after a tooltip is closed with functionAfter: function() {selectors = null;} but that had no effect.
I have a Codepen of the error here that should make it clearer.
// set list to be tooltipstered
$(".commands > li").tooltipster({
interactive: true,
theme: "tooltipster-light",
functionInit: function(instance, helper) {
var content = $(helper.origin).find(".tooltip_content").detach();
instance.content(content);
},
functionReady: function() {
selectors = $("ul.alternates > li");
$(selectors).click(function() {
var sampleData = $(this).text();
insertText(sampleData);
});
},
// this doesn't work
functionAfter: function() {
selectors = null;
}
});
// Begin inputting of clicked text into editor
function insertText(data) {
var cm = $(".CodeMirror")[0].CodeMirror;
var doc = cm.getDoc();
var cursor = doc.getCursor(); // gets the line number in the cursor position
var line = doc.getLine(cursor.line); // get the line contents
var pos = {
line: cursor.line
};
if (line.length === 0) {
// check if the line is empty
// add the data
doc.replaceRange(data, pos);
} else {
// add a new line and the data
doc.replaceRange("\n" + data, pos);
}
}
var code = $(".codemirror-area")[0];
var editor = CodeMirror.fromTextArea(code, {
mode: "simplemode",
lineNumbers: true,
theme: "material",
scrollbarStyle: "simple",
extraKeys: { "Ctrl-Space": "autocomplete" }
});
body {
margin: 1em auto;
font-size: 16px;
}
.commands {
display: inline-block;
}
.tooltip {
position: relative;
opacity: 1;
color: inherit;
}
.alternates {
display: inline;
margin: 5px 10px;
padding-left: 0;
}
.tooltipster-content .alternates {
li {
list-style: none;
pointer-events: all;
padding: 15px 0;
cursor: pointer;
color: #333;
border-bottom: 1px solid #d3d3d3;
span {
font-weight: 600;
}
&:last-of-type {
border-bottom: none;
}
}
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.25.2/theme/material.min.css" rel="stylesheet"/>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/235651/jquery-3.2.1.js"></script>
<script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/235651/tooltipster.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.25.2/codemirror.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.25.2/addon/mode/simple.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.25.2/addon/hint/show-hint.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.25.2/addon/scroll/simplescrollbars.js"></script>
<div class="container">
<div class="row">
<div class="col-md-6">
<ul class="commands">
<li><span class="command">Hover for my list</span><div class="tooltip_content">
<ul class="alternates">
<li>Lorep item</li>
<li>Ipsum item</li>
<li>Dollar item</li>
</ul>
</li>
</div>
</ul>
</div>
<div class="col-md-6">
<textarea class="codemirror-area"></textarea>
</div>
</div>
</div>
Tooltipster's functionReady fires every time the tooltip is added to the DOM, which means every time a user hovers over the list, you are binding the event again.
Here are two ways to prevent this from happening:
Attach a click handler to anything that exists in the DOM before the tooltip is displayed. (Put it outside of tooltipspter(). No need to use functionReady.)
Example:
$(document).on('click','ul.alternates li', function(){
var sampleText = $(this).text();
insertText(sampleText);
})
Here's a Codepen.
Unbind and bind the event each time functionReady is triggered.
Example:
functionReady: function() {
selectors = $("ul.alternates > li");
$(selectors).off('click').on('click', function() {
var sampleData = $(this).text();
insertText(sampleData);
});
}
Here's a Codpen.
You are binding new clicks every time.
I would suggest different code style but in that format you can just add before the click event
$(selectors).unbind('click');
Then do the click again..
Related
I want to be able to close my dropdown menu not only by clicking the x, but by clicking outside of it aswell. My js code doesnt seem to work. The Javascript is copied from a template i had left somewhere but im actually not able to fix it in order for it to work.
window.onclick = function closeMenu() {
if(document.getElementById("dropdown-content").style.left != "-300px") {
var dropdown = document.getElementById("dropdown-content");
var i;
for(i = 0; i < dropdown.length; i++) {
var openDropdown = dropdown[i];
if(openDropdown.style.left != ('-300px')) {
openDropdown.style.left = ('-300px');
}
}
}
}
.dropdown-content {
position: fixed;
background-color: rgb(255, 255, 255);
width: 300px;
height: 100%;
/*box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);*/
z-index: 600;
transition: 0.3s;
}
#dropdown-content {
left: -300px;
z-index: 600;
}
<div class="dropdown-container">
<div class="dropdown-content" id="dropdown-content">
<div class="menubutton" onclick="menu(this)">
<div class="bar1"></div>
<div class="bar2"></div>
<div class="bar3"></div>
</div>
<div class="menulist">
Angebot des Tages
Alle Angebote
Technik
Hardware
Mode
Automobil
</div>
</div>
</div>
const x = document.querySelector('.x');
const ul = document.querySelector('ul');
x.addEventListener('click', () => {
ul.classList.toggle('show');
});
document.addEventListener('click', ({
target
}) => {
if (target.matches('.x') === false) {
ul.classList.remove('show');
}
});
ul {
display: none;
}
ul.show {
display: block;
}
<div class="x">X</div>
<ul>
<li>Test</li>
<li>Test</li>
<li>Test</li>
<li>Test</li>
</ul>
Here, we track the X and just use toggle(), for any other click we ensure it is not X and then just remove() our show class.
If you are using just vanilla javascript, you can add a eventListener to the document object that listens to click event and checking some condition of the dropdown to check if it's opened, if it is, then closes it, if it's not, do nothing.
Something like:
document.addEventListener('click', () => {
const dropdown = ... // grab dropdown element
if (isOpened(dropdown)) closeDropdown()
})
EDIT: Also you should check if the click happened outside your dropdown since it will also be triggered if it is clicked. For that you can use the Node API.
Leaving it as:
document.addEventListener('click', (e) => {
const dropdown = ... // grab dropdown element
if (dropdown.contains(e.target)) return; // if the clicked element is inside the dropdown or it's the dropdown itself, do nothing
if (isOpened(dropdown)) closeDropdown()
})
The problem: I am not able to get the value of the HTML list from javascript. Each time user click next button, the program will count how many times the user clicks the next button. In javascript, I called the HTML list and iterate it. inside for loop, I called user click on next button number and add 1, The result of the sum will go inside the array and display that array line data. I don't know because of some reason I can't able to get data. Let me know if you get confused
Here is the HTML code
<div id="Border" class="">
<div id="Topic_List" class="creature">
<ul id="ListName" class="">
<li> Google </li>
<li>Facebook</li>
<li> Google </li>
</ul>
</div>
</div>
<div id="" class="" style="clear:both;"></div>
<div id="InputInsideFullCover" class="">
<textarea id="File_Name" name="File_Name"></textarea>
</div>
<button id="clickme" onclick="Next()"> Next </button>
<button onclick="Prev()"> Prev </button>
Here is the JavaScript code
<script>
function Next(){
var button = document.getElementById("clickme"),
count = 0;
button.onclick = function() {
count += 1;
var ul = document.getElementById("ListName");
var items = ul.getElementsByTagName("li");
for (var i = 0; i < items.length; ++i) {
var GetButtonClickValue = count;
var AddOne = GetButtonClickValue + 1;
var file_name = document.getElementById("ListName").innnerHTML=items[AddOne];
document.getElementById("File_Name").href = file_name;
var url_to_file = "http://www.example.com/"+file_name;
$.ajax({
url: url_to_file,
type:'HEAD',
error: function()
{
alert('data not found.');
},
success: function()
{
}
});
}
if(count > 9){
count = " ";
count = 1;
button.innerHTML = "Click me: " + count;
}
};
}
</script>
here is the CSS code
<style>
#Border{margin:5px auto;padding:0;width:50px;height:auto;border:1px solid #666;background-color:#f1f1f1}
#InputInsideFullCover{margin:5px;padding:0;width:700px;height:auto;}
#File_Name{margin:0;padding:0;width:638px;height:25px;resize:none;}
#Topic_List{margin:5px;padding:0;width:640px;height:auto;}
#Topic_List ul{margin:0px;padding:0;height:auto;width:12px;}
#Topic_List li{margin:0px;padding:0;list-style-type:none;float:left}
#button{
background-color: #4CAF50; /* Green background */
border: 1px solid green; /* Green border */
color: white; /* White text */
padding: 10px 24px; /* Some padding */
cursor: pointer; /* Pointer/hand icon */
width: 50%; /* Set a width if needed */
display: block; /* Make the buttons appear below each other */
}
.btn-group button:not(:last-child) {
border-bottom: none; /* Prevent double borders */
}
/* Add a background color on hover */
.btn-group button:hover {
background-color: #3e8e41;
}
</style>
You are declaring count to 0 at the beginning of the function. So every time the button is clicked count equals 0 and then one is added. (It will always be 1) You need to pull the count var outside of the function. This will also be useful for your Prev function. I bet that your Next and prev functions will be similar enough to make only one function. Call your function with true or false to add or subtract the count.
Just Add this
<script src="jquery-3.4.0.min.js"></script>
in your head tag before the external script tag
beacuse you are using ajax you need jquery
I'm having a trouble to get rid of unwanted counting-increment from forEach method in my CodePen.
The algorithm is simple:
EventManager() registers an event called mouseenter to every each of menuCells.
menuCount() gets an current index of the targeted cell. Next, matches it between a new node's index for showing or hiding a slateCell.
slateCount() gets the targeted item from the menuCount(), and using forEach() for getting li's index.
The problem is every time when I restart the event, an increment of the forEach() itself is increasing time to time like this: (couldn't imagine better describing words. Limited vocabulary problem :|)
This may not be a big problem because what the function does is actually just getting an index. But since I noticed this was abnormal, I wanted to know why and how to get rid of that unwanted increment counting.
I've been trying to find how to resolve my case or such as mine but still haven't found any of infos or articles.
Are there any solutions to fix this problem?
CodePen
'use strict';
const Slater = (function() {
let menu = document.querySelector('.menu'),
slate = document.querySelector('.slate');
let node_menuCells = menu.querySelectorAll('.cell'),
node_slateCells = slate.querySelectorAll('.grid.first > .cell');
let menuCells = Array.from(node_menuCells);
function EventManager(array, node) {
array.reduce((init, length, current) => {
node[current].addEventListener('mouseenter', (e) => menuCount(e, current, node_slateCells));
}, 0);
}
function menuCount(event, index, node) {
console.log(`menuCell count is: ${index}`);
node.forEach((item, i) => {
let comparing = (i == index) ? item.classList.add('shown') : item.classList.remove('shown');
slateCount(item);
})
}
function slateCount(item) {
let node_cellItems = item.querySelectorAll('li');
node_cellItems.forEach((listItem, n) => {
listItem.addEventListener('mouseenter', (e) => {
console.log(`slateCell count is: ${n}`);
})
})
}
return {
initialize: EventManager(menuCells, node_menuCells)
}
}());
* {
margin: 0;
padding: 0;
color: white;
}
ul li {
list-style: none;
text-decoration: none;
padding: 20px 0;
}
.layout {
width: 900px;
display: flex;
flex-flow: row;
align-items: center;
background-color: #414141;
}
.menu {
height: 60px;
}
.cell {
margin: 0 20px;
font-family:'Helvetica';
}
.slate {
border-top: 1px solid rgb(160, 117, 0);
height: 20rem;
}
.grid {
width: 50%;
height: 100%;
border: 1px solid rgb(160, 117, 0);
}
.grid > .cell {
display: none;
position: absolute;
color: rgb(36, 88, 21);
}
.shown {
display: block !important;
}
<div class="menu layout">
<div class="cell">Lorem</div>
<div class="cell">Ipsum Dolor</div>
<div class="cell">Consectetur</div>
<div class="cell">Similique</div>
</div>
<div class="slate layout">
<div class="grid first">
<ul class="cell">
<li>Sample Text 001</li>
<li>Sample Text 002</li>
</ul>
<ul class="cell">
<li>Sample Text 003</li>
<li>Sample Text 004</li>
</ul>
</div>
<div class="grid second">
<ul class="cell">
<li>Sample Text 001</li>
<li>Sample Text 002</li>
</ul>
<ul class="cell">
<li>Sample Text 003</li>
<li>Sample Text 004</li>
</ul>
</div>
</div>
From your code, every time you hover the top menu, a for-loop is ran to add event listeners on the slate items. So if you hover the slate items for the first time, the behavior is the same as you would expect, logging just once. However, if you repeat the action of hovering the menu, more and more of the same event listeners will be added to the slate items, so the log starts to blow up quickly, causing memory leaks.
To solve this, extract the logic of adding event listeners into the init function so that it will only be executed once.
function EventManager(array, node) {
array.reduce((init, length, current) => {
node[current].addEventListener('mouseenter', (e) => menuCount(e, current, node_slateCells));
}, 0);
// add the event listeners here
node_slateCells.forEach(item => slateCount(item));
}
function menuCount(event, index, node) {
console.log(`menuCell count is: ${index}`);
node.forEach((item, i) => {
let comparing = (i == index) ? item.classList.add('shown') : item.classList.remove('shown');
// slateCount(item);
})
}
function slateCount(item) {
let node_cellItems = item.querySelectorAll('li');
node_cellItems.forEach((listItem, n) => {
listItem.addEventListener('mouseenter', (e) => {
console.log(`slateCell count is: ${n}`);
})
})
}
I have a bunch of elements of the same type that have different parents, but I would like to be able to seamlessly navigate/cycle through all of them as if they were together.
<div>
<a href="#" class="open></a>
</div>
<div>
</div>
<div>
</div>
‹
›
I've managed to get this far: https://jsfiddle.net/pj0ecxge/
Currently it doesn't function as intended, as prev() and next() are only meant to target sibling elements, so the arrows don't work if the previous or next element is in another parent.
A single element will always be open by default, but it won't always be the same element as shown in the example. Also, only one element can be open at the same time.
If it makes a difference, I can add a single class to all children elements, but I can't change the HTML structure i.e put them all inside the same parent.
It would be nice if the navigation is infinite - i.e clicking next while the last element is open will show the first element and vice versa, but this is not required if it's too complex to do.
Thanks in advance and any help will be very appreciated!
You can check whether there are next/previous elements, if not then you can move a layer up/down like
$('.prev').click(function(e) {
e.preventDefault();
var current = $('.open');
var prev = current.prev();
if (!prev.length) {
prev = current.parent().prev('div').children('a:last-child')
}
if (prev.length) {
current.removeClass('open');
prev.addClass('open');
}
});
$('.next').click(function(e) {
e.preventDefault();
var current = $('.open');
var next = current.next();
if (!next.length) {
next = current.parent().next('div').children('a:first-child')
}
if (next.length) {
current.removeClass('open');
next.addClass('open');
}
});
* {
margin: 0;
padding: 0;
box-sizing: border-box;
text-align: center;
}
div {
font-size: 0;
}
div a {
width: 150px;
height: 150px;
border: 2px solid black;
}
a {
display: inline-block;
}
.open {
background: red;
}
.prev,
.next {
font-size: 100px;
text-decoration: none;
margin: 50px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
</div>
<div>
</div>
<div>
</div>
‹
›
Find the next set when the current set has reached either end. And the if the set is the last one then go back to the first one (and vice-versa).
$('.prev').click(function(e) {
e.preventDefault();
var current = $('.open');
var prev = current.prev();
if (!prev.length) {
prev = current.parent().prev('div').children('a:last-of-type');
if (!prev.length) {
prev = $('div:last-of-type').children('a:last-of-type');
}
}
current.removeClass('open');
prev.addClass('open');
});
$('.next').click(function(e) {
e.preventDefault();
var current = $('.open');
var next = current.next();
if (!next.length) {
next = current.parent().next('div').children('a:first-of-type');
if (!next.length) {
next = $('div:first-of-type').children('a:first-of-type');
}
}
current.removeClass('open');
next.addClass('open');
});
I'm trying to create a drag and drop feature in HTML5 where I can drag from one list to another. I have one list with draggable items and another list with items that have drop events added. The problem is, regardless of what element I drop onto, the last drop event that was added is the one that gets called.
Thanks for any help or suggestions.
I've included my code below:
<!DOCTYPE html>
<head>
<title>List Conversion Test</title>
<style type="text/css">
#list, #cart {
display: inline;
float: left;
border: 1px solid #444;
margin: 25px;
padding: 10px;
}
#list p {
background-color: #036;
color: #fff;
}
#cart p {
background-color: #363;
color: #fff;
}
.listitem {
}
.listitem_done {
text-decoration: line-through;
}
.product {
background-color: #CCC;
}
.product_over {
background-color: #363;
}
</style>
<script type="text/javascript" src="http://html5demos.com/js/h5utils.js"></script>
</head>
<body>
<article>
<div id="list">
<p>On My List</p>
<ul>
<li class="listitem" id="L001">Shopping List Item #1</li>
<li class="listitem" id="L002">Shopping List Item #2</li>
</ul>
<div id="done">
<p>In My Cart</p>
<ul></ul>
</div>
</div>
<div id="cart">
<p>Cart</p>
<ul>
<li class="product" id="P001">Product #1</li>
<li class="product" id="P002">Product #2</li>
</ul>
</div>
</article>
<script>
// make list items draggable
var list = document.querySelectorAll('li.listitem'), thisItem = null;
for (var i = 0; i < list.length; i++) {
thisItem = list[i];
thisItem.setAttribute('draggable', 'true');
addEvent(thisItem, 'dragstart', function (e) {
e.dataTransfer.effectAllowed = 'copy';
e.dataTransfer.setData('Text', this.id);
});
}
// give products drop events
var products = document.querySelectorAll('li.product'), thisProduct = null;
for (var i = 0; i < products.length; i++) {
thisProduct = products[i];
addEvent(thisProduct, 'dragover', function (e) {
if (e.preventDefault) e.preventDefault();
this.className = 'product_over';
e.dataTransfer.dropEffect = 'copy';
return false;
});
addEvent(thisProduct, 'dragleave', function () {
this.className = 'product';
});
addEvent(thisProduct, 'drop', function (e) {
//alert(thisProduct.id);
if (e.stopPropagation) e.stopPropagation();
var thisItem = document.getElementById(e.dataTransfer.getData('Text'));
thisItem.parentNode.removeChild(thisItem);
thisProduct.className = 'product';
handleDrop(thisItem, thisProduct);
return false;
});
}
// handle the drop
function handleDrop(i, p) {
alert(i.id + ' to ' + p.id);
var done = document.querySelector('#done > ul');
done.appendChild(i);
i.className = 'listitem_done';
}
</script>
</body>
</html>
This is why it's often a bad idea to define functions (such as callback functions) within a loop. You're assigning thisProduct within the loop, but it will be reassigned for the next iteration of the loop. The way your closures are set up, each callback is bound to the same variable thisProduct, and will use the latest value.
One possible fix is to create a new closure where thisProduct is needed such as
(function(thisProduct) {
addEvent(thisProduct, 'drop', function (e) {
//alert(thisProduct.id);
if (e.stopPropagation) e.stopPropagation();
var thisItem = document.getElementById(e.dataTransfer.getData('Text'));
thisItem.parentNode.removeChild(thisItem);
thisProduct.className = 'product';
handleDrop(thisItem, thisProduct);
return false;
});
}(thisProduct));
This jsFiddle seems to work for me now. See here for more explanation.