I have a component that should be reusable within a page. And it should be called only when in view.
I've added an extra class function outside class Tabs which is called class Pagination.
this class is not within class Tabs which is breaking the component if reused within the page (multiple times).
Would anyone help me to move the class Pagination inside Tabs? My goal is having a unique class, so I can reuse the component multiple times and only when is in view. I really hope it makes sense,
here you have a Pen demo, as you can see the pagination is not in the right constuctor. I brake the function everytime I try to move it.
class Pagination {
constructor(tabComponent, prevBtnId, nextBtnId) {
this.arrayUtils = new ArrayUtils();
this.tabComponent = tabComponent;
this.prevBtn = document.querySelector(prevBtnId);
this.nextBtn = document.querySelector(nextBtnId);
// listen to tabComponents newly created Toggle event
// in which we wanna make sure to disable Btns or something..
this.tabComponent.onTabsToggle((tabs, tabIndex) => {
this.applyPaginationRules(tabs, tabIndex);
});
}
setDisabled(btn, value) {
if (value) {
btn.setAttribute("disabled", "true");
} else {
btn.removeAttribute("disabled");
}
}
applyPaginationRules(tabs, newActiveIndex) {
const nextBtnDisabled = newActiveIndex === tabs.length - 1;
const prevBtnDisabled = newActiveIndex === 0;
this.setDisabled(this.nextBtn, nextBtnDisabled);
this.setDisabled(this.prevBtn, prevBtnDisabled);
}
paginate(btn, action) {
const block = btn.closest(".tabs-block");
const panels = block.querySelectorAll(".panel");
const tabs = block.querySelectorAll(".tab-wrapper > li > button");
const activeIndex = Array.from(tabs).findIndex(
(t) => t.getAttribute("data-open") === "true"
);
if (tabs.length < 2) {
console.log("0 OR 1 tabs to toggle so no action.");
return;
}
var newActiveIndex;
if (action === "next") {
newActiveIndex = this.arrayUtils.nextIndex(activeIndex, tabs);
} else if (action === "prev") {
newActiveIndex = this.arrayUtils.previousIndex(activeIndex, tabs);
} else {
throw "Invalid toggle action " + action;
}
// enable / disable next and previous btns..
this.applyPaginationRules(tabs, newActiveIndex);
this.tabComponent.toggleTabs(tabs[newActiveIndex], panels);
}
addPaginationListener(btn, action) {
btn.addEventListener("click", (e) => {
this.paginate(btn, action);
});
}
init() {
this.addPaginationListener(this.prevBtn, "prev");
this.addPaginationListener(this.nextBtn, "next");
// disable prev button on beggining since we start at 0..
this.setDisabled(this.prevBtn, true);
}
}
class ArrayUtils {
// getting next index in array
nextIndex(currentIndex, array) {
// if 0 OR 1 elements, index stays the same..
if (array.length < 2) return currentIndex;
// if possible increment.
if (currentIndex < array.length - 1) {
return currentIndex + 1;
}
// if index would exceed array size go to start.
return 0;
}
// getting previous INdex in array:
previousIndex(currentIndex, array) {
// if 0 OR 1 elements, index stays the same..
if (array.length < 2) return currentIndex;
// if possible decrement!
if (currentIndex > 0) {
return currentIndex - 1;
}
// start at the end of array when end is reached ofc.
return array.length - 1;
}
}
class Tabs {
constructor() {
this.tabsBlocks = document.querySelectorAll(".tabs-block");
this.onToggleHandlers = [];
}
onTabsToggle(fn) {
this.onToggleHandlers.push(fn);
}
emitTabsToggle(tabs, tabIndex) {
this.onToggleHandlers.forEach((fn) => fn(tabs, tabIndex));
}
init() {
if (this.tabsBlocks.length > 0) {
Array.prototype.forEach.call(this.tabsBlocks, (tabBlock, index) => {
const tabContainer = tabBlock.querySelector(".tab-wrapper");
const tabs = tabBlock.querySelectorAll(".tab-wrapper li button");
const panels = tabBlock.querySelectorAll(".panel");
tabContainer.setAttribute("role", "tablist");
Array.prototype.forEach.call(tabs, (tab, tabIndex) => {
if (tab.dataset.open === "true") this.toggleTabs(tab, panels);
tab.setAttribute("role", "tab");
tab.setAttribute(
"aria-controls",
`panel-${tab.dataset.target}-block-${index + 1}`
);
const associatedPanel = tabBlock.querySelector(
`[data-panel="${tab.dataset.target}"]`
);
if (associatedPanel !== null) {
associatedPanel.id = `panel-${tab.dataset.target}-block-${
index + 1
}`;
tab.id = `tab-${tab.dataset.target}-block-${index + 1}`;
}
tab.addEventListener("click", () => {
this.toggleTabs(tab, panels);
this.emitTabsToggle(tabs, tabIndex);
});
});
Array.prototype.forEach.call(panels, (panel) => {
const associatedTab = tabBlock.querySelector(
`[data-target="${panel.dataset.panel}"]`
);
panel.setAttribute("role", "tabpanel");
panel.setAttribute("aria-labelledby", `${associatedTab.id}`);
});
});
}
}
toggleTabs = (currentTab, panels) => {
const tabs = currentTab.closest(".tabs-block").querySelectorAll("button");
const target = currentTab.dataset.target;
Array.prototype.forEach.call(tabs, (tab) => {
if (tab.dataset.target !== target) {
tab.classList.remove("is-active");
tab.setAttribute("data-open", "false");
tab.setAttribute("aria-selected", "false");
}
});
Array.prototype.forEach.call(panels, (panel) => {
if (panel.dataset.panel !== target) {
panel.classList.remove("is-active");
} else {
panel.classList.add("is-active");
}
});
/// activate tab..
currentTab.classList.add("is-active");
currentTab.setAttribute("data-open", "true");
currentTab.setAttribute("aria-selected", "true");
};
}
const components = {
Tabs: new Tabs()
};
components.Tabs.init();
// have the pagination more decoupled from tabs!
// it uses tabs component but you can remove it OR apply it to other
// classes like so more easily..
const prevBtnId = ".pagination-prev";
const nextBtnId = ".pagination-next";
const pagination = new Pagination(components.Tabs, prevBtnId, nextBtnId);
pagination.init();
.tabs-block {
border: 1px solid red;
}
.tabs-block .tab-wrapper li {
flex: 1 1 0%;
text-align: center;
}
.tabs-block .tab-wrapper li button {
font-weight: lighter;
font-size: rem(20px);
}
.tabs-block .tab-wrapper li button.is-active {
font-weight: normal;
}
.tabs-block .panel {
display: none;
}
.tabs-block .panel.is-active {
display: block;
}
.is-active {
color: red;
}
<section class="tabs-block">
<ul class="tab-wrapper">
<li><button data-target="1" data-open="true">Tab title 1</button></li>
<li><button data-target="2">Tab title 2</button></li>
<li><button data-target="3">Tab title 3</button></li>
</ul>
<div class="panel-wrapper">
<div data-panel="1" class="panel">
<p>Panel 1 content</p>
</div>
<div data-panel="2" class="panel">
<p>Panel 2 content</p>
</div>
<div data-panel="3" class="panel">
<p>Panel 3 content</p>
</div>
</div>
<button class="buttonPrev pagination-prev">
<< Prev</button>
<button class="buttonNext pagination-next">Next >></button>
</section>
Related
There are some components stacked on top of each other and the last component has a timer. I want the timer to start only when that component is visible on screen or when scroll is reached to that component. [REPL]
let count_val = 80;
let count = 0;
function startTimer() {
let count_interval = setInterval(() => {
count += 1;
if(count >= count_val) {
clearInterval(count_interval);
}
}, 100);
}
// check if scroll reached to component and run below function.
startTimer();
How do I achieve this?
Like commented this can be achieved using Intersection Observer and an action
REPL
<script>
let count_val = 80;
let count = 0;
function timer(node) {
let interval
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if(entry.isIntersecting) {
interval = setInterval(() => {
count += 1;
if(count === count_val) {
clearInterval(interval);
}
}, 100);
} else {
clearInterval(interval)
count = 0
}
})
})
observer.observe(node)
return {
destroy: () => observer.disconnect()
}
}
</script>
<div use:timer>
<p>
Counter - {count}
</p>
</div>
<style>
div {
height: 100vh;
display: grid;
place-items: center;
background-color: teal;
color: #0a0a0a;
font-size: 4rem;
}
</style>
I have some simple vanilla accordion and I'm not really sure why the CSS transition is not applying here? The divs have correct height, what is going on here?
(Cannot post because apparently my issue is mostly code, so this text is a dirty fix, sorry).
HTML (simplified version)
<div class="container">
<ul>
<li class="accordion-item">
<button class="accordion-item__title">
Title
</button>
<div class="accordion-item__body not-active">
Body
</div>
</li>
</ul>
</div>
JS
document.addEventListener('DOMContentLoaded', function () {
const accordions = document.querySelectorAll('.block-accordion');
if (typeof (accordions) !== 'undefined' && accordions != null) {
//loop thorugh all acordions
for (let a = 0; a < accordions.length; a++) {
const accordion = accordions[a];
const accordionItems = accordion.querySelectorAll('.accordion-item');
//loop through all accordiond's items
for (let i = 0; i < accordionItems.length; i++) {
const accordionItem = accordionItems[i];
//show first by default
accordionItems[0].querySelector('.accordion-item__body').classList.remove('not-active');
accordionItems[0].querySelector('.accordion-item__body').parentElement.classList.add('active');
accordionItems[0].querySelector('.accordion-item__title').setAttribute("aria-expanded", true);
//hide each accordion on click
const accordionItemTitle = accordionItem.firstElementChild;
accordionItemTitle.addEventListener('click', function toggleAccordion(e) {
const accordionContent = accordionItem.querySelector('.accordion-item__body');
accordionContent.style.height = "auto";
if (accordionContent.previousElementSibling === e.target) {
accordionContent.classList.toggle('not-active');
accordionContent.parentElement.classList.toggle('active');
if (accordionContent.classList.contains('not-active')) {
accordionContent.style.height = '0px';
accordionContent.previousElementSibling.setAttribute("aria-expanded", false);
} else {
accordionContent.style.height = accordionContent.clientHeight + 'px';
accordionContent.previousElementSibling.setAttribute("aria-expanded", true);
}
}
});
}
}
}
});
SASS
.block-accordion {
.active {
display: block;
}
.not-active {
display: none;
transition: height 0.35s ease-in-out;
overflow: hidden;
}
}
I'm working on an assignment, where the user can draw inside an n*n grid. The tasks are as following:
Create a 16x16 grid as default grid upon page-load
Implement a mouseover-EventListener for the grid, that changes the background-color of cells to black
Add a reset button, that asks the user how big the new grid should be
Add 2 buttons that set the mouseover-EventListener (default black) to a different color. First button changes the color to yellow, second button to gray.
My problem:
I'm stuck with the last task. The 2 buttons work just fine. But they can be called only once. If I select yellow - it draws yellow. If I then select gray - it draws gray. After that, the button-clicks don't do anything anymore, but I can still draw in gray.
That's what I have done so far:
First, I querySelected the button-type and added an EventListener "click".
Second, in the colourHover function, a mouseover EventListener gets invoked, depending on the button's id.
Third, the EventListener-mouseover gets defined in the respective functions (grayColour(event), yellowColour(event)).
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
</head>
<body id="start">
<div class="container" id="grid">
</div>
<div><button type="button" class="button" id="gray">Gray Colour</button></div>
<div><button type="button" class="button" id="yellow">Yellow Colour</button></div>
<div><button type="button" id="button">Resize your grid manually.</button></div>
<script>
"use strict";
let container = document.getElementById("grid");
let n;
// creating a grid with number-input from user.
let makeGrid = function makeGrid(input) {
let c;
for (c = 0; c < (input * input); c++) {
let cell = document.createElement("div");
container.appendChild(cell).className = "squares";
};
container.style.gridTemplateColumns = `repeat(${input}, auto)`;
container.style.gridTemplateRows = `repeat(${input}, auto)`;
};
// function gets exectuted, only if a number and below 101
let numberCheck = function numberCheck() {
if ((n % n) === 0 && n <= 100) {
makeGrid(n);
};
while ((n % n) !== 0) {
alert("Please enter a number.");
n = prompt("Choose again.");
if ((n % n) === 0 && n <= 100) {
makeGrid(n);
}
};
while ((n % n) === 0 && n > 100) {
alert("The number is too high.");
n = prompt("Choose again.");
if ((n % n) === 0 && n <= 100) {
makeGrid(n);
}
else if ((n % n) !== 0) {
alert("Please enter a number");
n = prompt("Choose again");
if ((n % n) === 0 && n <= 100) {
makeGrid(n);
}
}
};
};
// default grid on first pageload
makeGrid(16);
// this function sets the hovered items colour to black on default / first pageload
let blackColor = function blackColor(event) {
let colour = event.target;
if (colour.className === "container") {
return;
}
else if (colour.className === "squares") {
colour.style.backgroundColor = "black";
};
};
// addEventListener for blackColor function
let hoverItems = document.querySelectorAll(".container");
hoverItems.forEach(element => { element.addEventListener("mouseover", blackColor)
});
// function to manual selecting a grid-size. Beforehand, old grid gets deleted
let removeElements = function removeElements(event) {
let elements = document.getElementsByClassName("squares");
while (elements.length > 0) {
elements[0].parentNode.removeChild(elements[0]);
};
n = prompt("Choose the size of your grid");
numberCheck(n);
};
// button for selecting a grid-size manually
let button = document.querySelector("#button");
button.addEventListener("click", removeElements);
// this function sets the background-color of the cells on mouseover to gray
let grayColour = function grayColour(event) {
let colour = event.target;
if (colour.className === "container") {
return;
}
else if (colour.className === "squares") {
colour.style.backgroundColor = "gray"
}
}
// this function sets the background-color of the cells on mouseover to yellow
let yellowColour = function yellowColour(event) {
let colour = event.target;
if (colour.className === "container") {
return;
}
else if (colour.className === "squares") {
colour.style.backgroundColor = "yellow";
}
}
let colourHover = function colourHover(event) {
let hover = document.querySelectorAll(".container");
switch (event.target.id) {
case "gray":
//hover.forEach(element => { element.removeEventListener("mouseover", yellowColour)
//});
//hover.forEach(element => { element.removeEventListener("mouseover", grayColour)
//});
hover.forEach(element => { element.addEventListener("mouseover", grayColour)
});
break;
case "yellow":
//hover.forEach(element => { element.removeEventListener("mouseover", yellowColour)
//});
//hover.forEach(element => { element.removeEventListener("mouseover", grayColour)
//});
hover.forEach(element => { element.addEventListener("mouseover", yellowColour)
});
break;
};
};
let colourBtn = document.querySelectorAll(".button");
colourBtn.forEach(element => { element.addEventListener("click", colourHover)
});
</script>
</body>
</html>
I appreciate any hint in the right direction. Really stuck right now.
The simplest way is to use CSS classes for styling the cells, and change the class of the table body in a button click handler. The state of the "widget" should be stored in a JS variable, that way you can avoid unnecessary DOM traversing on each click of the buttons. Something like this:
const buttons = document.querySelectorAll('button'),
table = document.querySelector('.hovered-colors');
// The state of the widget
let currentColor = 'black';
buttons.forEach(button => {
button.addEventListener('click', e => {
const newColor = e.target.getAttribute('data-color');
table.classList.remove(currentColor);
table.classList.add(newColor);
currentColor = newColor;
});
});
.black td:hover {
color: white;
background-color: black;
}
.gray td:hover {
color: black;
background-color: gray;
}
.yellow td:hover {
color: black;
background-color: yellow;
}
<table>
<tbody class="hovered-colors black">
<tr><td>R1C1</td><td>R1C2</td></tr>
<tr><td>R2C1</td><td>R2C2</td></tr>
<tr><td>R3C1</td><td>R3C2</td></tr>
</tbody>
</table>
<button data-color="gray">Gray</button>
<button data-color="yellow">Yellow</button>
My question is simple I guess. I'm making a full calendar with Jquery. I want to know how can I do something like I'll show on the pictures.
User selected day 3 of month (it will appear as blue) and if he mouse hover on 8 all the numbers inside that range get a class. So.. the 4,5,6,7,8 get something. If he leaves 8 and return back 7 the 8 has no class added, should remove.
Calendar showing my idea, I did it manually:
Calendar showing that I have at this moment:
HTML
<div class="new-calendar-inside">
<div class="month-and-year-calendar">
<div class="left-arrow prev-month"></div>
<div class="month-year actual"></div>
<div class="right-arrow next-month"><i class="icon chevron right"></i>
</div>
</div>
<div class="calendar-days-list">
<table class="table table-bordered">
<tr class="days-of-the-week">
<th>S</th>
<th>T</th>
<th>Q</th>
<th>Q</th>
<th>S</th>
<th>S</th>
<th>D</th>
</tr>
</table>
</div>
</div>
<div class="calendar-buttons">
<button class="new-button no-border-button">Cancelar</button>
<button id="confirm" class="new-button no-border-button">Ok</button>
</div>
</div>
JQUERY
this.getCalendarTable().on("click", "td", function () {
var row = _this.getCalendarTable().find(".selected");
var rowOrange = _this.getCalendarTable().find(".selected-orange");
var table = _this.getCalendarTable();
if (_this.getContainer().find(".new-calendar.simple").hasClass("mini")) {
if ($(this).text() != "") {
if (_this.getContainer().find(".new-calendar.simple").hasClass("simple")) {
// $('.new-calendar.simple').find(".border-left").removeClass("border-left");
if ($(this).text() != "") {
if (row.length < 2) {
$(this).addClass("selected");
// ADICIONAR HOVER COM RATO ENQUANTO ESCOLHE ULTIMO
var days = $('.table td'),
first = days.index($('td.selected:first')),
last = 60;
var rangeDays = days.slice(first, last);
$(rangeDays).on("mouseover, mouseout", function () {
last = $(days).index($(this));
$(this).addClass("active");
});
_this.getContainer().find(".border-left").removeClass("border-left");
_this.getContainer().find(".border-right").removeClass("border-right");
_this.getContainer().find(".selected").eq(0).addClass("border-left");
_this.getContainer().find(".selected").eq(1).addClass("border-right");
_this.firstNumberCalendar();
$('td:contains(31)').addClass("border-right");
} else if (row.length > 1) {
row.removeClass("selected");
table.find(".active").removeClass("active");
$(this).addClass("selected");
}
if (row.length == 1) {
var last = days.index($('td.selected:last'));
var newSlice = days.slice(first, last);
newSlice.addClass("active");
$(rangeDays).off('mouseover');
$(rangeDays).off('mouseout');
}
}
}
Probably I need to get the last position as mouse hover but I don't know how to do it.
Regards.
Don't use mouseover and mouseout for the exact reason you've encountered.
Instead keep track of the start and end indices during the mouse events. Then simply compare if each day is between that range.
Make sure to take into account that end may be < start if the user selects in reverse order.
Also make sure to consider that the mouse may be released while hovering on no particular day.
1 click / drag:
let div = document.querySelector('div');
for (let i = 0; i < 31; i++) {
let span = document.createElement('span');
span.textContent = i + 1;
div.appendChild(span);
span.addEventListener('mousedown', () => beginSelection(i));
span.addEventListener('mousemove', () => updateSelection(i));
span.addEventListener('mouseup', () => endSelection(i));
}
document.addEventListener('mouseup', () => endSelection());
let selecting, start, end;
let beginSelection = i => {
selecting = true;
start = i;
updateSelection(i);
};
let endSelection = (i = end) => {
updateSelection(i);
selecting = false;
};
let updateSelection = i => {
if (selecting)
end = i;
[...document.querySelectorAll('span')].forEach((span, i) =>
span.classList.toggle('selected', i >= start && i <= end || i >= end && i <= start));
};
div {
display: flex;
width: 200px;
flex-wrap: wrap;
}
span {
width: 30px;
border: 1px solid;
padding: 5px;
user-select: none;
}
span.selected {
background: #adf;
}
span:hover:not(.selected) {
background: #cfefff;
}
<div><div>
2 clicks:
let div = document.querySelector('div');
for (let i = 0; i < 31; i++) {
let span = document.createElement('span');
span.textContent = i + 1;
div.appendChild(span);
span.addEventListener('click', () => toggleSelection(i));
span.addEventListener('mousemove', () => updateSelection(i));
}
let selecting, start, end;
let toggleSelection = i => {
if (selecting)
endSelection(i);
else
beginSelection(i);
};
let beginSelection = i => {
selecting = true;
start = i;
updateSelection(i);
};
let endSelection = (i = end) => {
updateSelection(i);
selecting = false;
};
let updateSelection = i => {
if (selecting)
end = i;
[...document.querySelectorAll('span')].forEach((span, i) =>
span.classList.toggle('selected', i >= start && i <= end || i >= end && i <= start));
};
div {
display: flex;
width: 200px;
flex-wrap: wrap;
}
span {
width: 30px;
border: 1px solid;
padding: 5px;
user-select: none;
}
span.selected {
background: #adf;
}
span:hover:not(.selected) {
background: #cfefff;
}
<div>
<div>
I'm using fancybox to display image gallery.
I have this layout:
<div class="avatar">
<a class="avatar-item" data-fancybox="group" data-caption="Caption #1" href="img/avatars/jessica1.jpeg">
<img src="./img/avatars/jessica1.jpeg" width="145" height="145" alt="">
</a>
<a class="avatar-item" data-fancybox="group" data-caption="Caption #2" href="img/avatars/jessica2.jpeg">
<img src="./img/avatars/jessica2.jpeg" alt="">
</a>
</div>
And when I click on preview - gallery popup occurs, but it shows 4 images instead of 2. I included fancybox via data-attributes, without javascript. Tried magnific popup with gallery option - got same result.
link href attribute value and internal image src attribute are the same.
I don't have a thumbnail, display image cropped with css.
Hee is CSS:
.avatar {
&.slick-dotted.slick-slider {
margin-bottom: 0;
}
a.avatar-item {
width: 146px;
height: 146px;
display: inline-block;
}
width: 146px;
height: 146px;
border: 4px solid #FFF;
float: left;
position: relative;
overflow: hidden;
img {
height: 100%;
}
}
Found the problem. I use slick-slider before with infinite: true parameter, which creates an extra slides, so I got count slides x2
I have extended the _run function. Now duplicate images are removed from the gallery and still the correct key is returned. Furthermore I have the possibility to arrange the images in any order via data-facybox position.
So it is also possible to use the infinity version of the slick slider.
function _run(e, opts) {
var tempItems,
items = [],
newItem,
reOrder,
duplicates = false,
pos,
index = 0,
$target,
value,
instance;
// Avoid opening multiple times
if (e && e.isDefaultPrevented()) {
return;
}
e.preventDefault();
opts = opts || {};
if (e && e.data) {
opts = mergeOpts(e.data.options, opts);
}
$target = opts.$target || $(e.currentTarget).trigger("blur");
instance = $.fancybox.getInstance();
if (instance && instance.$trigger && instance.$trigger.is($target)) {
return;
}
if (opts.selector) {
tempItems = $(opts.selector);
} else {
// Get all related items and find index for clicked one
value = $target.attr("data-fancybox") || "";
if (value) {
tempItems = e.data ? e.data.items : [];
tempItems = tempItems.length ? tempItems.filter('[data-fancybox="' +
value + '"]') : $('[data-fancybox="' + value + '"]');
} else {
tempItems = [$target];
}
}
if (tempItems.length > 1) {
$.each(tempItems, function(key, item) { // prevents duplicate images in the gallery
newItem = false;
if (typeof item !== 'undefined') {
if (items.length > 0) {
var found = items.filter(function (items) { return items.href ==
item.href });
if (found.length == 0) {
newItem = true;
} else {
duplicates = true;
}
} else {
newItem = true;
}
}
if (newItem) {
// Sort if the attribute data-fancybox-pos exists
if ($(item).data('fancybox-pos')) {
reOrder = true;
pos = $(item).data('fancybox-pos') - 1;
if (pos in items) {
tempItem = items[pos];
items[pos] = item;
items.push(tempItem);
} else {
items[pos] = item;
}
} else {
items.push(item);
}
}
});
} else {
items = tempItems;
}
if (duplicates || reOrder) {
// find correct index if there were duplicates
$.each(items, function(key, item) {
if (item.href == $target[0].href) {
index = key;
}
});
} else {
index = $(items).index($target);
}
// Sometimes current item can not be found
if (index < 0) {
index = 0;
}
instance = $.fancybox.open(items, opts, index);
// Save last active element
instance.$trigger = $target;
}