Vue Carousel on iOS Safari only works with first slide - css

Good morning, I've build a custom vue carousel and implemented it into my progressive web app. Works fine on Chrome Android, whereas on iOS Safari - id doesn't. I received feedback that the carousel only allows to slide the first page and on the second - it doesn't react.
The problem is, I don't own Mac or Iphone and for now I can't test it myself. Could someone help me out and test the carousel on aforementioned devices? Here is the fiddle.
https://jsfiddle.net/LSliwa/97cpgq3z/
HTML:
<div id="app">
<Carousel>
<div v-for="(todo, index) in todos" :key="index">
<div class="app__element">
{{ todo.text }}
</div>
</div>
</Carousel>
</div>
CSS:
#app {
padding-top: 1rem;
}
.app__element {
text-align: center;
border: 1px solid red;
padding: 1rem;
margin: 0 1rem;
}
/* carousel styles */
.carousel {
overflow-x: hidden;
overflow-y: visible;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.carousel-navdots {
display: flex;
justify-content: center;
margin-bottom: 2rem;
}
.carousel-navdot {
height: 10px;
width: 10px;
border-radius: 50%;
background-color: gray;
margin: 0 5px;
transition: all 0.5s;
cursor: pointer;
}
.active {
background-color: #05AA19;
}
.carousel-wrapper {
display: flex;
align-items: stretch;
cursor: grab;
}
.carousel-wrapper:active {
cursor: grabbing;
}
.carousel-wrapper>div,
.carousel-wrapper>p,
.carousel-wrapper>span,
.carousel-wrapper>ul {
width: 100%;
flex-shrink: 0;
position: relative;
}
.scrolling {
transition: transform 0.5s;
}
.inactive {
flex-direction: column;
}
#media (min-width: 1024px) {
.inactive {
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
}
}
JS:
Vue.component('Carousel', {
template: '<div class="carousel"><div class="carousel-navdots" v-if="isActive" v-show="pagination"><div class="carousel-navdot" :class="{ \'active\': n == currentPage + 1 }" v-for="n in numberOfPages" v-show="numberOfPages > 1" :key="n" ref="navdot" #click="scrollWithNavdots(n)"></div></div><div class="carousel-wrapper a-stretch" :class="{ \'inactive\': !isActive }" :style="{ transform: `translateX(${translate}px)` }" ref="wrapper" v-on="isActive && active ? { touchstart: onTouchStart, touchmove: onTouchMove, touchend: onTouchEnd, mousedown: onTouchStart, mousemove: onTouchMove, mouseup: onTouchEnd } : {}"><slot></slot></div></div>',
props: {
active: {
type: Boolean,
default: () => true
},
activeOnViewport: {
type: Array,
default: () => [[1, true]]
},
columns: {
type: Array,
default: () => [[1, 1]]
},
pagination: {
type: Boolean,
default: () => true
},
sensitivity: {
type: Number,
default: () => 40
},
startFromPage: {
type: Number,
default: () => 0
},
autoplay: {
type: Boolean,
default: () => false
},
autoplaySpeed: {
type: Number,
default: () => 5
},
// usuń custom length jeżeli rerendering z :key zadziała
customLength: {
type: Number
}
},
data() {
return {
currentPage: this.startFromPage,
numberOfColumns: 1,
moveStart: null,
move: null,
currentTranslate: 0,
length: this.customLength == null ? this.$slots.default.length : this.customLength,
viewportColumnsMatched: null,
isActive: null,
mousedown: false,
elementWidth: 0,
autoplayInterval: null,
animateTimeout: null,
test: {
touchmove: false,
touchstart: false,
}
}
},
computed: {
maxScrollLeft() {
return this.currentPage == 0 ? true : false;
},
maxScrollRight() {
return this.currentPage + 1 == this.numberOfPages ? true : false;
},
numberOfPages() {
return Math.ceil(this.length / this.numberOfColumns);
},
sortedColumns() {
return this.columns.sort((a, b) => {
return a[0] - b[0];
});
},
sortedActive() {
return this.activeOnViewport.sort((a, b) => {
return a[0] - b[0];
})
},
translate() {
return -this.elementWidth * this.currentPage
}
},
watch: {
currentPage() {
this.animateCarousel();
this.$emit('change-page', this.currentPage);
},
startFromPage() {
this.currentPage = this.startFromPage;
},
// usuń watch customLength jeżeli rerendering z :key zadziała
customLength() {
this.length = this.customLength;
if (this.currentPage > this.length - 1) {
this.currentPage = this.length - 1;
}
}
},
methods: {
animateCarousel() {
this.$refs.wrapper.classList.add('scrolling');
this.animateTimeout = setTimeout(() => {
this.$refs.wrapper.classList.remove('scrolling');
}, 500);
},
scrollWithNavdots(index) {
this.currentPage = index - 1;
this.currentTranslate = parseFloat(this.$refs.wrapper.style.transform.slice(11, -3));
},
onTouchStart() {
clearInterval(this.autoplayInterval);
if (event.type == 'touchstart') {
this.moveStart = event.touches[0].screenX
} else {
this.moveStart = event.screenX;
this.mousedown = true;
}
},
onTouchMove() {
let translate;
if (event.type == 'touchmove') {
this.move = event.touches[0].screenX - this.moveStart;
} else if (event.type == 'mousemove' && this.mousedown == true) {
this.move = event.screenX - this.moveStart
}
if (this.move < 0 && this.maxScrollRight || this.move > 0 && this.maxScrollLeft) {
translate = this.translate + this.move*0.2;
} else {
translate = this.translate + this.move*0.5;
}
this.$refs.wrapper.style.transform = `translateX(${translate}px)`;
},
onTouchEnd() {
this.test.touchstart = false;
this.test.touchmove = false;
if (Math.abs(this.move) > this.sensitivity) {
if (this.move > 0 && !this.maxScrollLeft) {
this.currentPage--
} else if (this.move < 0 && !this.maxScrollRight) {
this.currentPage++
} else {
this.animateCarousel();
}
} else if (Math.abs(this.move) < this.sensitivity && Math.abs(this.move) > 1) {
this.animateCarousel();
}
this.$refs.wrapper.style.transform = `translateX(${this.translate}px)`;
this.mousedown = false;
this.moveStart = null;
this.move = null;
},
setColumns() {
this.viewportColumnsMatched = false;
this.sortedColumns.forEach(cur => {
if (window.matchMedia(`(min-width: ${cur[0]}px)`).matches) {
this.viewportColumnsMatched = true;
this.numberOfColumns = cur[1];
this.$refs.wrapper.childNodes.forEach(cur => {
cur.style.width = `${100/this.numberOfColumns}%`;
});
}
});
if (!this.viewportColumnsMatched) {
this.numberOfColumns = 1;
this.$refs.wrapper.childNodes.forEach(cur => {
cur.style.width = '100%';
});
}
setTimeout(() => {
this.elementWidth = this.$slots.default[0].elm.offsetWidth;
});
},
setActive() {
this.sortedActive.forEach(cur => {
if (window.matchMedia(`(min-width: ${cur[0]}px)`).matches) {
this.isActive = cur[1];
}
});
},
runCarousel() {
if (this.autoplay) {
this.autoplayInterval = setInterval(() => {
this.currentPage++
if (this.currentPage == this.numberOfPages) this.currentPage = 0;
}, this.autoplaySpeed * 1000);
}
},
},
mounted() {
this.setColumns();
this.setActive();
this.runCarousel();
window.addEventListener('resize', () => {
this.setColumns();
this.setActive();
});
},
destroyed() {
clearInterval(this.autoplayInterval);
clearTimeout(this.animateTimeout);
}
});
new Vue({
el: "#app",
data: {
todos: [
{ text: "Learn JavaScript", done: false },
{ text: "Learn Vue", done: false },
{ text: "Play around in JSFiddle", done: true },
{ text: "Build something awesome", done: true }
]
},
})
Thank you in advance for your help, good people.

Related

Create component with CSS class depending on props given

Is there a way in Vue.js to create a component that is containing CSS class scoped on which some values are given as parameters (props) before his creation.
Like:
<BeautifulBar :colorSelected="red" :colorDefault="blue", :animDuration="10.0s"></BeautifulBar
And in the component itself:
<template>
<div :class="'bar ' + (selected ? 'selected animated' : 'default')" #click="select()">The Bar</div>
</template>
<script>
export default{
data: function () { return { selected: false } },
props: {
colorSelected : {default: "orange", type: String},
colorDefault: {default: "orange", type: String},
animDuration: {default: "4.0s", type: String},
},
method: {
select: function() { this.selected = !this.selected;}
}
}
</script>
/* HERE */
<style scoped>
selected: {
color: red,
background-color: var(this.colorSelected)
}
default: {
color: black,
background-color: var(this.colorDefault)
}
animate:{
-webkit-transition: var(animDuration) !important;
-moz-transition: var() !important;
-o-transition: var() !important;
transition: var() !important;
}
</style>
Have you tried using computed function for class?
computed:{
classObject:function(){
return {
'selected': this.selected,
'animated': this.selected,
'default': !this.selected
}
}
}
then
<div :class="classObject">
your template
</div>
I succeed doing it like this (showing a progress bar looping animation, you can pass the animation time as props):
<template>
<div class="progress" :style="`height: ${height}rem;`">
<div
:class="'progress-bar bg-success ' + 'progressAnimation'"
role="progressbar"
:style="`width: ${progressValue}%; --animationDuration: ${animationDurationStr};`"
:aria-valuenow="`${progressValue}`"
aria-valuemin="0"
aria-valuemax="100"
></div>
</div>
</template>
<script scoped>
import config from "../config";
export default {
props: {
animationDuration: { type: Number, default: 140 },
height: { type: Number, default: 0.25 }
},
created: function() {
config.log("------------ AutoLoopProgressBar -----------");
config.log(
"autoloopprogressbar",
"Set animation duration to " + this.animationDurationStr
);
},
mounted: function() {
config.log("autoloopprogressbar", "Setup timer");
setInterval(() => {
this.reset();
}, this.animationDuration * 1000);
this.reset();
},
data: function() {
return {
progressValue: 0
};
},
computed: {
animationDurationStr: function() {
return this.animationDuration + ".0s";
}
},
methods: {
reset: function() {
let tmp = this.animationDuration;
this.animationDuration = 0;
this.progressValue = 0;
setTimeout(() => {
this.animationDuration = tmp;
this.progressValue = 100;
}, 0);
}
}
};
</script>
<style scoped>
.progressAnimation {
--animationDuration: 1s;
-webkit-transition: var(--animationDuration) linear !important;
-moz-transition: var(--animationDuration) linear !important;
-o-transition: var(--animationDuration) linear !important;
transition: var(--animationDuration) linear !important;
}
</style>

How to grey out the font as well as the checkbox

I would like to grey out the text within my filter section of a calendar app I am building but only seem to be able to code this onto the checkbox itself.
I have already tried passing this through my SCSS containers and labels with the ~ syntax to pass done from parent to child selectors. Cannot seem to find a way to solve this without breaking DRY principles. I have managed to get this to work on the checkbox but not the font within the same container.
Checkbox/index.js
import React from 'react'
import './index.css';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
import PropTypes from 'prop-types'
/*
based off the fake checkbox styles from here
should still be screen-reader/keyboard accessible
https://www.w3schools.com/howto/howto_css_custom_checkbox.asp
*/
class Checkbox extends React.Component {
render() {
const { name, label, value, checked, onChange, handleClose, dataObject, ...rest } = this.props
return (
<div {...rest}>
<label className="checkbox-container">
<span className="checkbox-label">{label}</span>
<input
type="checkbox"
checked={checked}
value={value}
onChange={onChange} />
<span className="checkmark"></span>
{this.props.handleClose && <FontAwesomeIcon onClick={this.handleClick.bind(this)} className='checkbox-icon' size="1x" icon={['fal', 'times']} />}
</label>
</div>
)
}
handleClick(e) {
e.preventDefault()
this.props.handleClose(this.props.dataObject)
}
}
Checkbox.propTypes = {
handleClose: PropTypes.func, // optional
onChange: PropTypes.func.isRequired,
label: PropTypes.string.isRequired,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]).isRequired,
dataObject: PropTypes.object, // optional -> used for the 'x' button
checked: PropTypes.bool.isRequired,
}
export default Checkbox
CalendersFiltersSection/index.js
import React from 'react'
import './index.css';
import PropTypes from 'prop-types'
import Checkbox from '../../../Checkbox'
import { defaultCalendarFilters} from '../../../../../constants'
class CalendarFiltersSection extends React.Component {
render() {
const { filters } = this.props
// let orderedFilters = Object.values(filters)
// orderedFilters.sort((a,b) => {
// return a.displayOrder - b.displayOrder
// })
return (
<section className="calendar-filters-section">
<div className='title'>{this.props.title}</div>
{ Object.values(filters).map(filter => {
let className = defaultCalendarFilters[filter.id].label.toLowerCase()
return <Checkbox className={`checkbox-event-type-${className}`} key={filter.id} label={filter.label} name={filter.id} checked={filter.checked} value={filter.id} onChange={this.props.handleInputChange} />
})
}
</section>
)
}
}
CalendarFiltersSection.propTypes = {
handleInputChange: PropTypes.func.isRequired,
filters: PropTypes.object.isRequired,
title: PropTypes.string.isRequired
}
export default CalendarFiltersSection
Checkbox/index.css
.checkbox-container {
display: block;
position: relative;
padding-left: 26px;
margin-bottom: 8px;
cursor: pointer;
font-size: 14px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* Hide the browser's default checkbox */
.checkbox-container input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
/* Create a custom checkbox */
.checkmark {
position: absolute;
top: 0;
left: 0;
height: 18px;
width: 18px;
background-color: #525259;
}
/* On mouse-over, add a grey background color */
/*.checkbox-container:hover input ~ .checkmark {
background-color: #ccc;
}
*/
/* When the checkbox is checked, add a blue background */
/*.checkbox-container input:checked ~ .checkmark {
background-color: #2196F3;
}*/
/* Create the checkmark/indicator (hidden when not checked) */
.checkmark:after {
content: "";
position: absolute;
display: none;
}
/* Show the checkmark when checked */
.checkbox-container input:checked ~ .checkmark:after {
display: block;
}
/* Style the checkmark/indicator */
.checkbox-container .checkmark:after {
left: 7px;
top: 3px;
width: 3px;
height: 7px;
border: solid black;
border-width: 0 3px 3px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
box-sizing: initial;
color: var(--secondary-font-color);
}
.checkbox-event-type-trainings .checkbox-container input:checked ~ .checkmark {
background-color: var(--event-type-trainings-color);
}
.checkbox-event-type-medizinisch .checkbox-container input:checked ~ .checkmark {
background-color: var(--event-type-medizinisch-color);
}
.checkbox-event-type-spiele .checkbox-container input:checked ~ .checkmark {
background-color: var(--event-type-spiele-color);
}
.checkbox-event-type-tests .checkbox-container input:checked ~ .checkmark {
background-color: var(--event-type-tests-color);
}
.checkbox-event-type-andere .checkbox-container input:checked ~ .checkmark {
background-color: var(--event-type-andere-color);
}
.checkbox-label {
color: var(--secondary-font-color);
}
.checkbox-icon {
position:absolute;
right: 0;
top: 4px;
z-index:5;
}
.checkbox-container .checkbox-icon {
visibility: hidden;
}
.checkbox-container:hover .checkbox-icon {
visibility: visible;
}
CalendarFiltersSection/index.css
.calendar-filters-section {
align-self: flex-start;
color: var(--secondary-font-color);
margin-bottom: 4rem;
}
Constants.js
import { history } from './util/history'
export const API_HOST = process.env.REACT_APP_USE_STAGING_API ?
"http://example.com" : "http://127.0.0.1:8000"
export const EVENTS_LIST_URL = `${API_HOST}/events/`
export const CALENDARS_LIST_URL = `${API_HOST}/users/calendars/`
export const USERS_LIST_URL = `${API_HOST}/users/`
export const getTeamsListUrl = (clubId) =>
`${API_HOST}/clubs/${clubId}/teams/`
export const getClubsTeamsPlayersListUrl = (clubId, teamId) =>
`${API_HOST}/clubs/${clubId}/teams/${teamId}/players/`
//TODO combine all of these into one data structure .
export const mapNameToEventTypeNumber = {
0: 'andere',
1: 'trainings',
2: 'tests',
3: 'medizinisch',
4: 'medizinisch',
5: 'medizinisch',
6: 'spiele',
8: 'medizinisch',
}
//TODO combine all of these into one data structure .
// updaet: the backend updated to use strings instead of numbers...
export const mapMainCategoryNameToSubCategoryName = {
social: 'andere',
practice: 'trainings',
assessment: 'tests',
doctor: 'medizinisch',
physio: 'medizinisch',
operation: 'medizinisch',
match: 'spiele',
meeting: 'andere',
}
// 'event_types': ((0, 'SOCIAL'),
// (1, 'PRACTICE'),
// (2, 'ASSESSMENT'),
// (3, 'DOCTOR'),
// (4, 'PHYSIO'),
// (5, 'OPERATION'),
// (6, 'MATCH'),
// (7, 'PRIVATE'),
// (8, 'MEETING'),
// (9, 'SCHOOL/JOB'),
// (10, 'MISC')),
export const mapSpecificEventTypeCategoryToEventTypeNumber = {
0: 'Social',
1: 'Practice',
2: 'Examination',
3: 'Doctor',
4: 'Rehab',
5: 'Physio',
6: 'Match',
7: 'Injury',
8: 'Treatment',
}
export const mappingOfSpecificEventTypeToFilterCategories = {
1: 0,
3: 1,
4: 1,
5: 1,
6: 2,
2: 3,
0: 4
}
export const defaultCalendarFilters = [
{
id: 0,
checked: true,
label: 'Trainings',
eventTypeNumbers: ['practice']
// eventTypeNumbers: ['1']
},
{
id: 1,
checked: true,
label: 'Medizinisch',
eventTypeNumbers: ['doctor','physio','operation']
// eventTypeNumbers: ['3','4','5']
},
{
id: 2,
checked: true,
label: 'Spiele',
eventTypeNumbers: ['match']
// eventTypeNumbers: ['6']
},
{
id: 3,
checked: true,
label: 'Tests',
eventTypeNumbers: ['assessment']
// eventTypeNumbers: ['2']
},
{
id: 4,
checked: true,
label: 'Andere',
eventTypeNumbers: ['meeting', 'misc']
// eventTypeNumbers: ['0']
},
]
export const mapClubRoleNumberToSlug = {
'trainer': 0,
'athletiktrainer': 1,
'torwarttrainer': 2,
'individualtrainer': 3,
'rehabilitationtrainer': 4,
'arzt': 5,
'physiotherapeut': 6,
'videoanalyst': 7,
'athlet': 8,
'betreuer': 9,
'clubadmin': 10,
}
export const clubRolesData = {
'trainer': {
slug: 'trainer',
roleNum: 0,
isStaff: true,
isAthlete: false
},
'athletiktrainer': {
slug: 'athletiktrainer',
roleNum: 1,
isStaff: true,
isAthlete: false
},
'torwarttrainer': {
slug: 'torwarttrainer',
roleNum: 2,
isStaff: true,
isAthlete: false
},
'individualtrainer': {
slug: 'individualtrainer',
roleNum: 3,
isStaff: true,
isAthlete: false
},
'rehabilitationtrainer': {
slug: 'rehabilitationtrainer',
roleNum: 4,
isStaff: true,
isAthlete: false
},
'arzt': {
slug: 'arzt',
roleNum: 5,
isStaff: true,
isAthlete: false
},
'physiotherapeut': {
slug: 'physiotherapeut',
roleNum: 6,
isStaff: true,
isAthlete: false
},
'videoanalyst': {
slug: 'videoanalyst',
roleNum: 7,
isStaff: true,
isAthlete: false
},
'athlet': {
slug: 'athlet',
roleNum: 8,
isStaff: false,
isAthlete: true
},
'betreuer': {
slug: 'betreuer',
roleNum: 9,
isStaff: true,
isAthlete: false
},
'clubadmin': {
slug: 'clubadmin',
roleNum: 10,
isStaff: true,
isAthlete: false
},
}
export const ATHLETE_ROLES = [
'athlet'
]
export const STAFF_ROLES = [
'arzt',
'athletiktrainer',
'betreuer',
// 'clubadmin',
'individualtrainer',
'physiotherapeut',
'rehabilitationtrainer',
'torwarttrainer',
'trainer',
'videoanalyst',
]
export const getUserData = () => {
let userData = null
const userDataJson = localStorage.userData
if(userDataJson) {
userData = JSON.parse(userDataJson)
} else {
history.push('./login')
return
}
return userData
}
export const NAVBAR_TABS = [
{
text: 'Dein Club',
link: '/club-member',
active: false,
icon: 'fa-shield'
},
{
text: 'Kalender',
link: '/calendar',
active: false,
icon: 'fa-calendar-alt'
},
{
text: 'Library',
link: '/library/abilities',
active: false,
icon: 'fa-books'
}
]

How should I loop a 360 degree image in CSS for an Ionic App?

I have been asked to implement some of our companies 360 degree photos (panorama sort of things) on to our company app.
So far I have only been able to get the image to go across then back again which doesn't give the smooth endless photo loop we are after.
I am using Ionic 4.
Here is my current CSS code
#keyframes example {
from {
left: 0;
}
to {
left: -1350px;
}
}
.three-sixty {
max-width: none !important;
position: absolute;
height: 250px;
animation-name: example;
animation-duration: 10s;
animation-iteration-count: infinite;
animation-direction: alternate;
animation-timing-function: linear;
}
<ion-col>
<img class="three-sixty" [src]="mainImage">
</ion-col>
I am assuming I need multiple of the same image with timed animations to achieve the endless loop?
Extra Info:
The start of the image fits perfectly with the end of the image - so I need to make sure the image runs on smoothly from the last.
Any help would be greatly appreciated.
Please check the following component I implemented to view a 360 image in ionic (for web and mobile) in the latest version of ionic:
SCSS:
.rotatable-image {
display: flex;
justify-content: center;
align-items: center;
height: 300px;
background-color: lightgrey;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
HTML:
<div
class="rotatable-image"
(mousedown)="handleMouseDown($event)"
(ondragstart)="preventDragHandler($event)"
>
<img
draggable="false"
class="rotatable-image"
alt=""
[src]="_dir + '/' + imageIndex + '.jpg'"
/>
<div style="font-size: 50px">
{{ imageIndex }}
</div>
</div>
TS:
import {
AfterViewInit,
Component,
Input,
OnChanges,
OnDestroy,
OnInit,
} from '#angular/core';
// You can play with this to adjust the sensitivity
// higher values make mouse less sensitive
const pixelsPerDegree = 3;
#Component({
selector: 'app-panoramic',
templateUrl: './panoramic.component.html',
styleUrls: ['./panoramic.component.scss'],
})
export class PanoramicComponent implements AfterViewInit, OnDestroy {
// tslint:disable-next-line: variable-name
_dir = '../../../assets/dummies/panoramic';
// tslint:disable-next-line: variable-name
_totalImages = 46;
#Input()
set dir(val: string) {
this._dir = val !== undefined && val !== null ? val : '';
}
#Input()
set totalImages(val: number) {
this._totalImages = val !== undefined && val !== null ? val : 0;
}
dragging = false;
dragStartIndex = 1;
dragStart?: any;
imageIndex = 1;
ngAfterViewInit() {
document.addEventListener(
'mousemove',
(evt: any) => {
this.handleMouseMove(evt);
},
false
);
document.addEventListener(
'mouseup',
() => {
this.handleMouseUp();
},
false
);
}
ngOnDestroy() {
document.removeEventListener(
'mousemove',
(evt: any) => {
this.handleMouseMove(evt);
},
false
);
document.removeEventListener(
'mouseup',
() => {
this.handleMouseUp();
},
false
);
}
handleMouseDown(e: any) {
this.dragging = true;
console.log(e.screenX);
this.dragStart = e.screenX;
this.dragStartIndex = this.imageIndex;
}
handleMouseUp() {
this.dragging = false;
}
updateImageIndex(currentPosition: any) {
const numImages = this._totalImages;
const pixelsPerImage = pixelsPerDegree * (360 / numImages);
// pixels moved
const dx = (currentPosition - this.dragStart) / pixelsPerImage;
let index = Math.floor(dx) % numImages;
if (index < 0) {
index = numImages + index - 1;
}
index = (index + this.dragStartIndex) % numImages;
if (index !== this.imageIndex) {
if (index === 0) {
index = 1;
}
this.imageIndex = index;
}
}
handleMouseMove(e: any) {
if (this.dragging) {
this.updateImageIndex(e.screenX);
}
}
preventDragHandler(e: any) {
e.preventDefault();
}
}

Make position: fixed behavior like sticky (for Vue2)

Position: sticky doesn't support by the most mobile browsers. But position: fixed is not that thing I need (because of fixed block overlaps content in the bottom of document).
I guess for jquery it will be easy to set static position for fixed block if we get bottom of document onscroll.
But for Vue2 I haven't any idea how to do the same. Give some advice please. Or maybe better solution exists.
As I mentioned in the comments, I'd recommend using a polyfill if at all possible. They will have put a lot of effort into getting it right. However, here is a simple take on how you might do it in Vue.
I have the application handle scroll events by putting the scrollY value into a data item. My sticky-top component calculates what its fixed top position would be, and if it's > 0, it uses it. The widget is position: relative.
new Vue({
el: '#app',
data: {
scrollY: null
},
mounted() {
window.addEventListener('scroll', (event) => {
this.scrollY = Math.round(window.scrollY);
});
},
components: {
stickyTop: {
template: '<div class="a-box" :style="myStyle"></div>',
props: ['top', 'scrollY'],
data() {
return {
myStyle: {},
originalTop: 0
}
},
mounted() {
this.originalTop = this.$el.getBoundingClientRect().top;
},
watch: {
scrollY(newValue) {
const rect = this.$el.getBoundingClientRect();
const newTop = this.scrollY + +this.top - this.originalTop;
if (newTop > 0) {
this.$set(this.myStyle, 'top', `${newTop}px`);
} else {
this.$delete(this.myStyle, 'top');
}
}
}
}
}
});
#app {
height: 1200px;
}
.spacer {
height: 80px;
}
.a-box {
display: inline-block;
height: 5rem;
width: 5rem;
border: 2px solid blue;
position: relative;
}
<script src="//unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<div class="spacer"></div>
<div class="a-box"></div>
<sticky-top top="20" :scroll-y="scrollY"></sticky-top>
<div class="a-box"></div>
</div>
This seem to work for me
...
<header
ref="header"
class="header-container"
:class="{ 'header-container--sticky': isHeaderSticky }"
>
...
...
data() {
return{
scrollY: null,
headerTop: 0,
isHeaderSticky: false,
}
},
mounted() {
window.addEventListener('load', () => {
window.addEventListener('scroll', () => {
this.scrollY = Math.round(window.scrollY);
});
this.headerTop = this.$refs.header.getBoundingClientRect().top;
});
},
watch: {
scrollY(newValue) {
if (newValue > this.headerTop) {
this.isHeaderSticky = true;
} else {
this.isHeaderSticky = false;
}
}
}
...
...
.header-container {
&--sticky {
position: sticky;
top: 0;
z-index: 9999;
}
}
...

Mobile Safari Crashing with background-position css

I have the following code which crashes in mobile safari, works in all other browsers. I have a checkbox that is using jquery plugin for displaying a nicer image when it is checked. The issue seems to be in the background-position attribute support in Safari-mobile but I don't know how to fix it.
<input type="checkbox" id="terms" data-bind="checked: termsOfUseAccepted" >
<label for="terms" class="" data-bind="text:TermsLabel"></label>
<div class="chk-overview ">
<h2 >Continue</h2>
</div>
Following Javascript is used
$(function () {
var isTouch = false;
try { isTouch = "ontouchstart" in window; } catch (e) { }
var $activeTip = null;
if (isTouch) {
document.ontouchstart = function () {
if ($activeTip) {
$activeTip.data("close").call($activeTip);
$activeTip = null;
}
};
}
function courseViewModel() {
var self = this;
self.termsOfUseAccepted = ko.observable(false);
self.TermsLabel = ko.observable('I understand');
self.continueDisplay = ko.computed({
read: function() {
return self.termsOfUseAccepted();
},
owner: this,
deferEvaluation: true
});
};
var viewModel = new courseViewModel();
ko.applyBindings(viewModel);
});
(function($) {
$.fn.hoverIntent = function(f, g) {
var cfg = {sensitivity: 7,interval: 100,timeout: 0};
cfg = $.extend(cfg, g ? {over: f,out: g} : f);
var cX, cY, pX, pY;
var track = function(ev) {
cX = ev.pageX;
cY = ev.pageY
};
var compare = function(ev, ob) {
ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t);
if ((Math.abs(pX - cX) + Math.abs(pY - cY)) < cfg.sensitivity) {
$(ob).unbind("mousemove", track);
ob.hoverIntent_s = 1;
return cfg.over.apply(ob, [ev])
} else {
pX = cX;
pY = cY;
ob.hoverIntent_t = setTimeout(function() {
compare(ev, ob)
}, cfg.interval)
}
};
var delay = function(ev, ob) {
ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t);
ob.hoverIntent_s = 0;
return cfg.out.apply(ob, [ev])
};
var handleHover = function(e) {
var ev = jQuery.extend({}, e);
var ob = this;
if (ob.hoverIntent_t) {
ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t)
}
if (e.type == "mouseenter") {
pX = ev.pageX;
pY = ev.pageY;
$(ob).bind("mousemove", track);
if (ob.hoverIntent_s != 1) {
ob.hoverIntent_t = setTimeout(function() {
compare(ev, ob)
}, cfg.interval)
}
} else {
$(ob).unbind("mousemove", track);
if (ob.hoverIntent_s == 1) {
ob.hoverIntent_t = setTimeout(function() {
delay(ev, ob)
}, cfg.timeout)
}
}
};
return this.bind('mouseenter', handleHover).bind('mouseleave', handleHover)
}
})(jQuery);
(function($) {
jQuery.fn.customInput = function () {
$(this).each(function (i) {
if ($(this).is('[type=checkbox],[type=radio]')) {
var input = $(this);
if (input.data('customInput') === 'done') {
return true;
}
else {
input.data('customInput', 'done');
}
// get the associated label using the input's id
var label = $('label[for=' + input.attr('id') + ']');
//get type, for classname suffix
var inputType = (input.is('[type=checkbox]')) ? 'checkbox' : 'radio';
// wrap the input + label in a div
$('<div class="custom-' + inputType + '"></div>').insertBefore(input).append(input, label);
// find all inputs in this set using the shared name attribute
var allInputs = $('input[name=' + input.attr('name') + ']');
// necessary for browsers that don't support the :hover pseudo class on labels
label.hover(
function () {
$(this).addClass('hover');
if (inputType == 'checkbox' && input.is(':checked')) {
$(this).addClass('checkedHover');
}
},
function () { $(this).removeClass('hover checkedHover'); }
);
//bind custom event, trigger it, bind click,focus,blur events
input.bind('updateState', function () {
if (input.is(':checked')) {
if (input.is(':radio')) {
allInputs.each(function () {
$('label[for=' + $(this).attr('id') + ']').removeClass('checked');
});
};
label.addClass('checked');
}
else { label.removeClass('checked checkedHover checkedFocus'); }
})
.trigger('updateState')
.click(function () {
if (input.is(':checked')) {
if (input.is(':radio')) {
allInputs.each(function () {
$('label[for=' + $(this).attr('id') + ']').removeClass('checked');
});
};
label.addClass('checked');
}
else { label.removeClass('checked checkedHover checkedFocus'); }
})
.focus(function () {
label.addClass('focus');
if (inputType == 'checkbox' && input.is(':checked')) {
$(this).addClass('checkedFocus');
}
})
.blur(function () { label.removeClass('focus checkedFocus'); });
}
});
};
})(jQuery);
$.fn.smartHover = function (configObject) {
if (isTouch) {
$(this)
.bind("hold", function () {
$activeTip = $(this);
$(this).data("held", true);
})
.bind("hold", configObject.over)
.bind("click", function (e) {
var wasHeld = $(this).data("held");
$(this).data("held", false);
if (wasHeld) {
e.preventDefault();
return false;
}
})
.data("close", configObject.out);
} else {
$(this).hoverIntent(configObject);
}
};
$('input').customInput();
And here is the css
.chk-overview h2 { font: 24px "StoneSansITCW01-SemiBol 735693",sans-serif; margin-bottom: 20px;padding: 0 }
.custom-checkbox label {
background: transparent url(http://aonhewittnavigators.com/AonExchange/media/Image-Gallery/SiteImages/checkbox.png) no-repeat;
outline: 0;
background-position: 0 0;
}
.custom-checkbox label {
cursor: pointer;
display: block;
height: 19px;
outline: 0;
position: relative;
width: 21px;
z-index: 1;
}
.custom-checkbox label.checked {
background-position: 0 bottom;
padding: 0;
}
.custom-checkbox input {
left: 1px;
margin: 0;
outline: 0;
position: absolute;
top: 5px;
z-index: 0;
height: 0;
}
Try removing the height: 0 on the checkbox style. I have seen it crash when the height or width attribute is set on the checkbox input.

Resources