I am working with the slider plugin Splide JS. And I pass the options like this (full render slider):
let custom_slider = new Splide(".custom_slider");
custom_slider.mount();
if (custom_slider.length === 2) {
custom_slider.options = {
perPage: 2,
breakpoints: {
550: {
perPage: 1,
},
},
};
}
All of the above options work, except for the breakpoints. And I really don't understand why this happens.
I'm trying to assign specific options depending on the installed slides count, getting it like this - custom_slider.length.
It's all good, it works, BUT NOT the breakpoint options.
You can try the following code:
let splide_length = document.querySelectorAll('.custom_slider .splide__slide').length;
if ( splide_length === 2 ) {
let custom_slider = new Splide(".custom_slider", {
perPage: 2,
breakpoints: {
550: {
perPage: 1,
},
},
}).mount();
} else {
let custom_slider = new Splide(".custom_slider").mount();
}
Related
In my Carousel, there is an option to show more than 1 item at a time. It is activated by selecting the number of items you wish to show (max 4). The code refers to this as perView.
Items to display example
This carousel is glide.js, so everything is modular. I have 2 things of note getting activated, the Controls (next/prev button) and Breakpoints (think media queries).
The goal is to turn off the controls in mobile only if perView equals anything other than NaN (not a number)... When it is inactive, it returns NaN... so I guess the comment would be:
if perView does not equal NaN, then controls = false
and because i need this in a mobile breakpoint, I would put the code in the breakpoint bracket.
import AEM from 'base/js/aem';
import Glide, { Controls, Breakpoints, Autoplay, Keyboard, Swipe } from '#glidejs/glide/dist/glide.modular.esm';
class Carousel extends AEM.Component {
init() {
this.initCarousel();
}
initCarousel() {
const el = this.element;
const props = this.props;
const pauseButton = this.element.querySelector('.pause-button');
let perView = parseInt(props.cmpPerView, 10);
let cmpDelay = parseInt(props.cmpDelay, 10);
let autoPlay = true;
if (this.props.cmpAutoplay === 'false' ||
this.props.carouselAutoplay === 'false' ||
!Number.isNaN(perView) && perView < 1 ||
window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
autoPlay = false;
}
this.modules = {
Controls: Controls,
Breakpoints: Breakpoints,
Autoplay: Autoplay,
Keyboard: Keyboard,
Swipe: Swipe
};
this.options = {
type: props.cmpType,
perView: Number.isNaN(perView) ? 1 : perView,
autoplay: autoPlay ? cmpDelay : false,
peek: 0,
keyboard: true,
animationDuration: 400,
rewind: true,
breakpoints: {
768: {
perView: Number.isNaN(perView) || perView === 1 ? 1 : 3,
},
578: {
peek: perView > 1 ? 125 : 0,
perView: 1,
controls: perView > 1,
},
375: {
peek: perView > 1 ? 85 : 0,
perView: 1,
}
}
};
const glide = new Glide(el, this.options);
if (pauseButton && autoPlay) {
const pauseClasses = pauseButton.classList;
glide.on('play', () => {
pauseClasses.add('fa-pause');
pauseClasses.remove('fa-play');
});
glide.on('pause', () => {
pauseClasses.add('fa-play');
pauseClasses.remove('fa-pause');
});
pauseButton.addEventListener('click', () => {
if (pauseClasses.contains('fa-play')) {
glide.play();
} else {
glide.pause();
}
});
} else if (pauseButton) {
pauseButton.remove();
}
if (window.Granite && window.Granite.author && window.Granite.author.MessageChannel) {
/*
* Editor message handling:
* - subscribe to "cmp.panelcontainer" message requests sent by the editor frame
* - check that the message data panel container type is correct and that the id (path) matches this specific
* - Carousel component. If so, route the "navigate" operation to enact a navigation of the Carousel based
* - on index data
*/
this.element.querySelectorAll('.glide__slide').forEach(e => {
if (!e.classList.contains('glide__slide--active')) {
e.classList.add('visually-hidden');
}
});
window.CQ = window.CQ || {};
window.CQ.CoreComponents = window.CQ.CoreComponents || {};
window.CQ.CoreComponents.MESSAGE_CHANNEL = window.CQ.CoreComponents.MESSAGE_CHANNEL ||
new window.Granite.author.MessageChannel('cqauthor', window);
window.CQ.CoreComponents.MESSAGE_CHANNEL.subscribeRequestMessage('cmp.panelcontainer', message => {
if (message.data && message.data.type === 'cmp-carousel' &&
message.data.id === el.dataset.cmpPanelcontainerId) {
if (message.data.operation === 'navigate') {
let index = message.data.index;
if (index < 0) {
return;
}
this.element.querySelectorAll('.glide__slide').forEach((slide, i) => {
if (i === index) {
slide.classList.add('glide__slide--active');
slide.classList.remove('visually-hidden');
} else {
slide.classList.remove('glide__slide--active');
slide.classList.add('visually-hidden');
}
});
}
}
});
} else {
glide.mount(this.modules);
}
}
}
export { Carousel };
So I have
controls: perView > 1
in the 578 breakpoint... but it doesn't work.
Using the Embla Carousel, React version, I'm trying to turn/on off the carousel based on breakpoint. So for mobile, it's on and for tablet up it's off. Here's what I tried that seems to work initially, but won't reinitialize. I'm guessing it's because destory was called so it can reInit and tried init, but no luck there either.
const emblaOptions = {};
const [viewportRef, emblaApi] = useEmblaCarousel(emblaOptions);
const handleEmblaInit = () => {
if (window.innerWidth > 768) {
emblaApi.destroy();
} else {
emblaApi.reInit(emblaOptions);
}
};
useEffect(() => {
if (emblaApi) {
handleEmblaInit();
window.addEventListener("resize", handleEmblaInit);
}
}, [emblaApi]);
Pass null instead of the emblaRef when you want it to be inactive, like demonstrated here:
https://github.com/davidcetinkaya/embla-carousel/issues/99#issuecomment-688730519
From version 7 and up you can use the active option together with the breakpoints option to achieve this. Example from the Embla Carousel docs:
const options = {
active: true,
breakpoints: {
'(min-width: 768px)': { active: false },
},
}
Usage with React:
const [emblaRef, emblaApi] = useEmblaCarousel({
active: true,
breakpoints: {
'(min-width: 768px)': { active: false },
},
});
In VUE I have a range slider component that I use to display different values at set point when the user drags the slider.
This is all working fine, the only problem that I have is that the slider VUE slider component is making my page not scrollable on mobile.
Is the browser getting confused somehow with the drag action, meaning it doesn't know it's happening on the slider but on the actual page?
Any ideas how I can solve this? Thanks
<div class='slider margin-top-10 margin-bottom-40'>
<range-slider
v-model="value"
:min="min"
:max="max"
:step="step"
:range="range"
:height="barheight"
:dot-height="dotheight"
:dot-width="dotwidth"
:piecewise-label="label"
:process-style="processstyle">
</range-slider>
</div>
import RangeSlider from 'vue-range-component'
export default {
components: {
RangeSlider
},
props: {
membership: {
type: Object,
},
translations: {
type: Object
},
isAgency: {
type: Boolean
},
clientsCap: {
type: Number
}
},
data: function() {
return {
value: 10,
min: 10,
max: 50,
step: 10,
data: [10, 20, 30, 40, 50,],
range: [{label: '10'}, {label: '20'}, {label: '30'}, {label: '40'}, {label: '50'}],
label: true,
barheight: 3,
dotwidth: 16,
dotheight: 16,
processstyle: { backgroundColor: 'transparent'}
}
},
created: function(){
this.$emit('updateImages', this.value);
},
watch: {
value: function(){
this.$emit('updateImages', this.value);
}
},
computed: {
price: function() {
var price = this.value * this.membership.additional_images;
if(this.isAgency)
price = price * this.clientsCap;
if(this.membership.priceOffered < this.membership.basePrice && this.membership.priceOffered !== undefined)
price = price - (price * 0.10);
return price;
}
}
}
This bug already fixed in github repo BUT npm repo not updated.
https://github.com/xwpongithub/vue-range-slider/blob/master/src/js/vue-range-slider.js#L1006
So you need remove installed package from npm
"vue-range-component": "^1.0.3",
and add directly from github
"vue-range-component": "xwpongithub/vue-range-slider",
A solution to this problem may be the following.
In the created function of Vue where you have this component place the following lines.
created () {
VueRangeSlider.methods.handleKeyup = ()=> console.log;
VueRangeSlider.methods.handleKeydown = ()=> console.log;
},
I also ran into the problem of entering text input fields and the fact that the scroll is blocked on mobile devices.
To solve it, I downloaded the library itself and deleted all the event processing methods and keys (keydown, keyup), this helped with a text input problem. For scrolling, you need to delete the line f = "touchstart"
You need to clear the line: x="" and w="" It's, rough, line 73 and 74
I am hoping you can help. I am fairly new to Unit Testing. I have a Karma + Jasmine set up which is running a PhantomJS browser. This is all good.
What I am struggling with is I have a link on the page, when this link is clicked it injects some HTML. I want to test that the HTML has been injected.
Now at this point, I have the test working but only sometimes, from what I can figure out if my JS runs fast enough the HTML gets injected before the expect() is run. If not the test fails.
How can I make my Jasmine test wait for all JS to finish executing before the expect() is run?
The test in question is it("link can be clicked to open a modal", function() {
modal.spec.js
const modalTemplate = require('./modal.hbs');
import 'regenerator-runtime/runtime';
import 'core-js/features/array/from';
import 'core-js/features/array/for-each';
import 'core-js/features/object/assign';
import 'core-js/features/promise';
import Modal from './modal';
describe("A modal", function() {
beforeAll(function() {
const data = {"modal": {"modalLink": {"class": "", "modalId": "modal_1", "text": "Open modal"}, "modalSettings": {"id": "", "modifierClass": "", "titleId": "", "titleText": "Modal Title", "closeButton": true, "mobileDraggable": true}}};
const modal = modalTemplate(data);
document.body.insertAdjacentHTML( 'beforeend', modal );
});
it("link exists on the page", function() {
const modalLink = document.body.querySelector('[data-module="modal"]');
expect(modalLink).not.toBeNull();
});
it("is initialised", function() {
spyOn(Modal, 'init').and.callThrough();
Modal.init();
expect(Modal.init).toHaveBeenCalled();
});
it("link can be clicked to open a modal", function() {
const modalLink = document.body.querySelector('[data-module="modal"]');
modalLink.click();
const modal = document.body.querySelector('.modal');
expect(modal).not.toBeNull();
});
afterAll(function() {
console.log(document.body);
// TODO: Remove HTML
});
});
EDIT - More Info
To further elaborate on this, The link Jasmine 2.0 how to wait real time before running an expectation put in the comments has helped me understand a bit better, I think. So what we are saying it we want to spyOn the function and wait for it to be called and then initiate a callback which then resolves the test.
Great.
My next issue is, if you look at the structure of my ModalViewModel class below, I need to be able to spyOn insertModal() to be able to do this, but the only function that is accessible in init(). What would I do to be able to move forward with this method?
import feature from 'feature-js';
import { addClass, removeClass, hasClass } from '../../01-principles/utils/classModifiers';
import makeDraggableItem from '../../01-principles/utils/makeDraggableItem';
import '../../01-principles/utils/polyfil.nodeList.forEach'; // lt IE 12
const defaultOptions = {
id: '',
modifierClass: '',
titleId: '',
titleText: 'Modal Title',
closeButton: true,
mobileDraggable: true,
};
export default class ModalViewModel {
constructor(module, settings = defaultOptions) {
this.options = Object.assign({}, defaultOptions, settings);
this.hookModalLink(module);
}
hookModalLink(module) {
module.addEventListener('click', (e) => {
e.preventDefault();
this.populateModalOptions(e);
this.createModal(this.options);
this.insertModal();
if (this.options.closeButton) {
this.hookCloseButton();
}
if (this.options.mobileDraggable && feature.touch) {
this.hookDraggableArea();
}
addClass(document.body, 'modal--active');
}, this);
}
populateModalOptions(e) {
this.options.id = e.target.getAttribute('data-modal');
this.options.titleId = `${this.options.id}_title`;
}
createModal(options) {
// Note: As of ARIA 1.1 it is no longer correct to use aria-hidden when aria-modal is used
this.modalTemplate = `<section id="${options.id}" class="modal ${options.modifierClass}" role="dialog" aria-modal="true" aria-labelledby="${options.titleId}" draggable="true">
${options.closeButton ? '<a href="#" class="modal__close icon--cross" aria-label="Close" ></a>' : ''}
${options.mobileDraggable ? '<a href="#" class="modal__mobile-draggable" ></a>' : ''}
<div class="modal__content">
<div class="row">
<div class="columns small-12">
<h2 class="modal__title" id="${options.titleId}">${options.titleText}</h2>
</div>
</div>
</div>
</section>`;
this.modal = document.createElement('div');
addClass(this.modal, 'modal__container');
this.modal.innerHTML = this.modalTemplate;
}
insertModal() {
document.body.appendChild(this.modal);
}
hookCloseButton() {
this.closeButton = this.modal.querySelector('.modal__close');
this.closeButton.addEventListener('click', (e) => {
e.preventDefault();
this.removeModal();
removeClass(document.body, 'modal--active');
});
}
hookDraggableArea() {
this.draggableSettings = {
canMoveLeft: false,
canMoveRight: false,
moveableElement: this.modal.firstChild,
};
makeDraggableItem(this.modal, this.draggableSettings, (touchDetail) => {
this.handleTouch(touchDetail);
}, this);
}
handleTouch(touchDetail) {
this.touchDetail = touchDetail;
const offset = this.touchDetail.moveableElement.offsetTop;
if (this.touchDetail.type === 'tap') {
if (hasClass(this.touchDetail.eventObject.target, 'modal__mobile-draggable')) {
if (offset === this.touchDetail.originY) {
this.touchDetail.moveableElement.style.top = '0px';
} else {
this.touchDetail.moveableElement.style.top = `${this.touchDetail.originY}px`;
}
} else if (offset > this.touchDetail.originY) {
this.touchDetail.moveableElement.style.top = `${this.touchDetail.originY}px`;
} else {
this.touchDetail.eventObject.target.click();
}
} else if (this.touchDetail.type === 'flick' || (this.touchDetail.type === 'drag' && this.touchDetail.distY > 200)) {
if (this.touchDetail.direction === 'up') {
if (offset < this.touchDetail.originY) {
this.touchDetail.moveableElement.style.top = '0px';
} else if (offset > this.touchDetail.originY) {
this.touchDetail.moveableElement.style.top = `${this.touchDetail.originY}px`;
}
} else if (this.touchDetail.direction === 'down') {
if (offset < this.touchDetail.originY) {
this.touchDetail.moveableElement.style.top = `${this.touchDetail.originY}px`;
} else if (offset > this.touchDetail.originY) {
this.touchDetail.moveableElement.style.top = '95%';
}
}
} else {
this.touchDetail.moveableElement.style.top = `${this.touchDetail.moveableElementStartY}px`;
}
}
removeModal() {
document.body.removeChild(this.modal);
}
static init() {
const instances = document.querySelectorAll('[data-module="modal"]');
instances.forEach((module) => {
const settings = JSON.parse(module.getAttribute('data-modal-settings')) || {};
new ModalViewModel(module, settings);
});
}
}
UPDATE
After working through it has been discovered that .click() events are asynchronous which is why I am gettnig the race issue. Documentation & Stack Overflow issues thoughtout the web recommend using createEvent() and dispatchEvent() as PhantomJs does not understand new MouseEvent().
Here is my code which is now trying to do this.
modal.spec.js
// All my imports and other stuff
// ...
function click(element){
var event = document.createEvent('MouseEvent');
event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
element.dispatchEvent(event);
}
describe("A modal", function() {
// Some other tests
// Some other tests
it("link can be clicked to open a modal", function() {
const modalLink = document.body.querySelector('[data-module="modal"]');
click(modalLink);
const modal = document.body.querySelector('.modal');
expect(modal).not.toBeNull();
});
// After all code
// ...
});
Unfortunately this is producting the same results. 1 step closer but not quite there.
After a touch of research, it looks as though your use of the click event is triggering an asynchronous event loop essentially saying "Hey set this thing to be clicked and then fire all the handlers"
Your current code can't see that and has no real way of waiting for it. I do believe you should be able to build and dispatch a mouse click event using the info here.
https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent
I think that should allow you to build a click event and dispatch it onto your element. The difference is that dispatchEvent is synchronous - it should block your test until the click handlers have completed. That should allow you to do your assertion without failures or race conditions.
I have finally found a solution.
There are 2 parts to this, the first part came from #CodyKnapp. His insight into a click() function running asynchronously helped to solve the first part of the issue.
Here is the code for this part.
modal.spec.js
// All my imports and other stuff
// ...
function click(element){
var event = document.createEvent('MouseEvent');
event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
element.dispatchEvent(event);
}
describe("A modal", function() {
// Some other tests
// Some other tests
it("link can be clicked to open a modal", function() {
const modalLink = document.body.querySelector('[data-module="modal"]');
click(modalLink);
const modal = document.body.querySelector('.modal');
expect(modal).not.toBeNull();
});
// After all code
// ...
});
This allowed for the code to run synchronously.
The second part was a poor understanding on my part of how to write Jasmine tests. In my original tests I was running Modal.init() inside of it("is initialised", function() { when actually I want to be running this inside of beforeAll(). This fixed the issue I had where my tests would not always be successful.
Here is my final code:
modal.spec.js
const modalTemplate = require('./modal.hbs');
import '#babel/polyfill';
import Modal from './modal';
function click(element){
var event = document.createEvent('MouseEvent');
event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
element.dispatchEvent(event);
}
describe("A modal", function() {
beforeAll(function() {
const data = {"modal": {"modalLink": {"class": "", "modalId": "modal_1", "text": "Open modal"}, "modalSettings": {"id": "", "modifierClass": "", "titleId": "", "titleText": "Modal Title", "closeButton": true, "mobileDraggable": true}}};
const modal = modalTemplate(data);
document.body.insertAdjacentHTML( 'beforeend', modal );
spyOn(Modal, 'init').and.callThrough();
Modal.init();
});
it("link exists on the page", function() {
const modalLink = document.body.querySelector('[data-module="modal"]');
expect(modalLink).not.toBeNull();
});
it("is initialised", function() {
expect(Modal.init).toHaveBeenCalled();
});
it("link can be clicked to open a modal", function() {
const modalLink = document.body.querySelector('[data-module="modal"]');
click(modalLink);
const modal = document.body.querySelector('.modal');
expect(modal).not.toBeNull();
});
afterAll(function() {
console.log(document.body);
// TODO: Remove HTML
});
});
I am currently working on a kendo scheduler.
I was asked by my client to implement a 3-days view, which I successfully did, but there is one problem : that custom view does not get the "k-state-selected" class when it is selected, which means it can't be fully stylized.
I failed to find why that could be the case : none of the examples for creating a custom time view I found mentionned anything about defining the class the view takes when selected, and furthermore, it does get the "k-state-hover" class when hovered. Strange.
Here's the (I think) relevant JS :
var ThreeDayView = kendo.ui.MultiDayView.extend({
nextDate: function () {
return kendo.date.nextDay(this.startDate());
},
options: {
selectedDateFormat: "{0:D} - {1:D}"
},
name: "ThreeDayView",
calculateDateRange: function () {
//create a range of dates to be shown within the view
var start = this.options.date,
idx, length,
dates = [];
for (idx = 0, length = 3; idx < length; idx++) {
dates.push(start);
start = kendo.date.nextDay(start);
}
this._render(dates);
}
});
$("#scheduler").kendoScheduler({
date: new Date(), // The current date of the scheduler
showWorkHours: true,
height: 600,
views: [
"week",
{ type: ThreeDayView, title: "3 Jours", selected: false },
"day"
],
editable:
{
resize: true,
move: true,
template: $("#templateEdition").html()
},
dataSource: finalSource,
add: onAdd,
edit: onUpdate,
remove: onDelete,
save: onSaving
})
});
Does anyone have any idea why that might be ?
Thanks !
The type or the view should be a string - the name of the custom view - "ThreeDayView" (instead of ThreeDayView) in this case.