We are developing a website with unique navigation. Part of it involves on each scroll either up or down, it fires JavaScript and navigates to a different HTML element. It is in Vue.js / Nuxt.
So far, everything works beautifully, minus the usage of the trackpad. The initial 2-finger swipe with the trackpad works -- however, this seems to initiate some sort of a smooth scroll, which takes a while to complete. If you try to 2-finger swipe again in the same direction, it's treated as one long scroll, which doesn't fire the JavaScript to advance to the next page. I did a console log of the deltaY and since it's a 2-finger swipe (smooth scroll?), the deltas take a second or two to finish.
This causes issues since you can't use the trackpad and swipe through sections quickly. You have to swipe down, wait until the scroll finishes, then swipe down again.
How would we fix this issue? Is there a way to kill the current scroll, or to eliminate smooth scrolling and just have scrolls go 100 deltas in one way or the other?
Thanks in advance
Vue.js code
export default {
layout: 'empty',
components: {
Footer,
Logo,
LottieAnimation,
ServiceIcon,
CloseBtn
},
async asyncData({ app }) {
try {
return await app.$api.$get('/services.json')
} catch (error) {
return {
error
}
}
},
data() {
return {
activePage: 0,
config: {},
error: null,
goToPageTimer: null,
isTouchpad: null,
page: {},
scrollDisabled: false,
scrollPercentage: 0,
scrollPercentageOld: 0,
scrollDirection: null,
scrollTimer: null,
touchStart: null,
touchTimer: null,
lastWheelDirection: null,
lastWheelEvent: null,
wheelEventsCount: 0,
wheelEventsLimit: 25,
wheelStopTime: 120
}
},
watch: {
scrollPercentage(newValue, oldValue) {
this.scrollDirection = newValue > oldValue ? 'bottom' : 'top'
if (this.scrollDirection === 'bottom') {
this.goToNextPage()
} else {
this.goToPreviousPage()
}
}
},
mounted() {
window.addEventListener('keydown', this.onKeyDown)
window.addEventListener('scroll', this.handleScroll)
window.addEventListener('wheel', this.onMouseWheel)
window.addEventListener('touchend', this.onTouchEnd)
window.addEventListener('touchstart', this.onTouchStart)
this.initPage()
},
destroyed() {
window.removeEventListener('keydown', this.onKeyDown)
window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('wheel', this.onMouseWheel)
window.removeEventListener('touchend', this.onTouchEnd)
window.removeEventListener('touchstart', this.onTouchStart)
},
methods: {
disableScrolling() {
this.scrollDisabled = true
if (this.goToPageTimer) {
clearTimeout(this.goToPageTimer)
}
this.goToPageTimer = setTimeout(() => {
this.scrollDisabled = false
}, 30)
},
getServicePageId(slug) {
let servicePageId = null
Object.keys(this.page.childPages).forEach((pageId) => {
if (this.page.childPages[pageId].slug === slug) {
servicePageId = parseInt(pageId)
}
})
return servicePageId
},
goToNextPage() {
if (
this.scrollDisabled ||
this.activePage === this.page.childPages.length
) {
return
}
this.activePage = this.activePage === null ? 0 : this.activePage + 1
if (this.activePage < this.page.childPages.length) {
this.scrollToActivePage()
}
},
goToPreviousPage() {
if (this.scrollDisabled) {
return
}
if (!this.activePage) {
return
}
this.activePage -= 1
this.scrollToActivePage()
},
goToPage(index) {
if (this.scrollDisabled) {
return
}
this.activePage = index
this.scrollToActivePage()
},
handleScroll() {
// If scrolling to top do nothing
if (!window.scrollY) {
return
}
if (this.activePage < this.page.childPages.length - 1) {
window.scrollTo(0, 0)
}
},
initPage() {
if (this.$route.query.service) {
this.activePage = this.getServicePageId(this.$route.query.service)
}
},
isServiceActiveOrIsLatestService(slug) {
const servicePageId = this.getServicePageId(slug)
// Service is active
if (this.activePage === servicePageId) {
return true
}
const latestServicePageId = this.page.childPages.length - 1
// Service is the latest and active page is over it (user is looking the footer)
return (
servicePageId === latestServicePageId &&
this.activePage > latestServicePageId
)
},
onKeyDown(e) {
const nextPageKeys = [
34, // Page down
39, // Arrow right
40 // Arrow down
]
const previousPageKeys = [
33, // Page up
37, // Arrow left
38 // Arrow up
]
if (nextPageKeys.includes(e.keyCode)) {
this.goToNextPage()
} else if (previousPageKeys.includes(e.keyCode)) {
this.goToPreviousPage()
}
},
onMouseWheel(event) {
const now = +new Date()
const millisecondsSinceLastEvent = this.lastWheelEvent
? now - this.lastWheelEvent
: 0
const eventsLimitReached = this.wheelEventsCount > this.wheelEventsLimit
const stopTime = this.wheelStopTime
const delta = Math.sign(event.deltaY)
const directionChanged = delta !== 0 && this.lastWheelDirection !== delta
this.lastWheelEvent = now
if (directionChanged) {
this.lastWheelDirection = delta
}
if (
!directionChanged &&
!eventsLimitReached &&
millisecondsSinceLastEvent &&
millisecondsSinceLastEvent <= stopTime
) {
this.wheelEventsCount += 1
return
}
this.wheelEventsCount = 0
if (delta === -1) {
this.goToPreviousPage()
} else {
this.goToNextPage()
}
},
onTouchEnd(e) {
const now = +new Date()
const touchEndX = e.changedTouches[0].clientX
const touchEndY = e.changedTouches[0].clientY
// Try to guess single touch based on touch start - touch end time
const isSingleTouch = now - this.touchTimer <= 100
// Single touch
if (isSingleTouch && this.touchStart.x === touchEndX) {
const horizontalPercentage = Math.ceil(
(touchEndX * 100) / window.innerWidth
)
const verticalPercentage = Math.ceil(
(touchEndY * 100) / window.innerHeight
)
if (horizontalPercentage <= 40) {
this.goToPreviousPage()
return
}
if (horizontalPercentage >= 60) {
this.goToNextPage()
return
}
if (verticalPercentage <= 40) {
this.goToPreviousPage()
return
}
if (verticalPercentage >= 60) {
this.goToNextPage()
return
}
}
// Touch move
if (this.touchStart.y > touchEndY + 5) {
this.goToNextPage()
} else if (this.touchStart.y < touchEndY - 5) {
this.goToPreviousPage()
}
},
onTouchStart(e) {
this.touchTimer = +new Date()
this.touchStart = {
x: e.touches[0].clientX,
y: e.touches[0].clientY
}
},
onVisibilityChanged(isVisible, entry) {
if (isVisible) {
entry.target.classList.add('dynamic-active')
} else {
entry.target.classList.remove('dynamic-inactive')
}
},
scrollToActivePage() {
this.scrollToService(this.page.childPages[this.activePage].slug)
},
scrollToRef(ref) {
if (!this.$refs[ref]) {
return
}
if (this.$refs[ref][0]) {
this.$scrollTo(this.$refs[ref][0], 100, {
force: true,
cancelable: false
})
} else {
this.$scrollTo(this.$refs[ref], 100, { force: true, cancelable: false })
}
this.disableScrolling()
},
scrollToService(slug) {
this.scrollToRef(`service-${slug}`)
},
serviceBgImageUrl(service) {
return require(`~/assets/img/services/backgrounds/${service.slug}.jpg`)
}
},
head() {
const data = {
bodyAttrs: {
class: 'services'
}
}
if (this.page.data) {
data.title = this.page.data.title
data.meta = this.page.metadata
}
return data
}
}
Related
I have created a countdown timer which decrease a number in the template perfectly, but now I need it to launch a function declared within methods after it reaches 0. I've tried to check if condition is met within methods but it doesn't launch anything when reaching 0.
Here is my index.vue
<template>
<div>
{{ timerCount }}
</div>
</template>
<script>
export default {
data(){
return{
timerCount: 60
}
},
watch: {
timerEnabled(value) {
if (value) {
setTimeout(() => {
this.timerCount = this.timerCount - 1;
}, 1000);
}
},
timerCount: {
handler(value) {
if (value > 0 && this.timerEnabled) {
setTimeout(() => {
this.timerCount = this.timerCount - 1;
}, 1000);
}
},
immediate: true
},
},
methods:{
launchThis() {
// I need this function to be launched if timerCount reaches 0
}
}
}
</script>
Any guidance to make it work will greatly appreciated.
You can use something like this
<script>
export default {
watch: {
timerCount: {
handler(value) {
if (value > 0 && this.timerEnabled) {
setTimeout(() => {
this.timerCount = this.timerCount - 1;
}, 1000);
} else {
this.launchThis() // run your function here
}
},
immediate: true
},
},
}
</script>
I have a stopwatch code using Vue 3 and Vuex that has functions that can start, stop and reset.
This is the code :
store/stopwatch/index.js
export default {
state: {
time: "00:10.000",
timeStarted: null,
timeBegan: null,
timeStopped: null,
stoppedDuration: 0,
started: null,
running: false,
temp: {
min: "0",
sec: "0",
ms: "0",
secondsPassed: 0
}
},
actions: {
start({ state, commit, dispatch }) {
if (state.running) return;
if (state.timeBegan === null) {
state.timeBegan = new Date();
}
if (state.timeStopped !== null) {
state.stoppedDuration += new Date() - state.timeStopped;
}
commit("start", {
callback: () => {
dispatch("clockRunning");
}
});
},
async clockRunning({ state, commit, dispatch }) {
let currentTime = new Date();
let timeElapsed = new Date(
currentTime - state.timeBegan - state.stoppedDuration
);
let min = timeElapsed.getUTCMinutes();
let sec = timeElapsed.getUTCSeconds();
let ms = timeElapsed.getUTCMilliseconds();
commit("newTemp", {
key: "secondsPassed",
value: parseInt(Math.abs((state.timeStarted - new Date()) / 1000), 10)
});
if (state.running) {
await dispatch("zeroPrefix", { num: min, digit: 2 }).then(
(zeroPrefixResponse) => {
commit("newTemp", {
key: "min",
value: zeroPrefixResponse
});
}
);
await dispatch("zeroPrefix", { num: sec, digit: 2 }).then(
(zeroPrefixResponse) => {
commit("newTemp", {
key: "sec",
value: zeroPrefixResponse
});
}
);
await dispatch("zeroPrefix", { num: ms, digit: 3 }).then(
(zeroPrefixResponse) => {
commit("newTemp", {
key: "ms",
value: zeroPrefixResponse
});
}
);
state.time =
state.temp.min + ":" + state.temp.sec + "." + state.temp.ms;
}
},
zeroPrefix(context, payload) {
return new Promise((resolve) => {
let zero = "";
for (let i = 0; i < payload.digit; i++) {
zero += "0";
}
resolve((zero + payload.num).slice(-payload.digit));
});
}
},
mutations: {
newTemp(state, payload) {
state.temp[payload.key] = payload.value;
},
addSecondPassed(state, second) {
state.temp.secondsPassed += second;
},
resetSecondPassed(state) {
state.temp.secondsPassed = 0;
},
start(state, payload) {
state.started = setInterval(() => {
payload.callback();
}, 10);
state.running = true;
},
stop(state) {
state.running = false;
state.timeStopped = new Date();
clearInterval(state.started);
},
reset(state) {
state.running = false;
clearInterval(state.started);
state.stoppedDuration = 0;
state.timeBegan = null;
state.timeStopped = null;
state.time = "00:10.000";
}
},
getters: {}
};
App.vue
<template>
<div>
<span class="time">{{ $store.state.stopwatch.time }}</span>
<br />
<button #click="start">Start</button>
<button #click="stop">Stop</button>
<button #click="reset">Reset</button>
</div>
</template>
<script>
export default {
methods: {
start() {
this.$store.dispatch("start");
},
stop() {
this.$store.commit("stop");
},
reset() {
this.$store.commit("reset");
},
},
};
</script>
This is the demo code on codesandbox
What happens with the code above by starting from the 10 seconds and then clicking the start button, seconds starting from the number 0 then 1,2,3. do not continue from number 10.
How to start stopwatch from state time with 10 seconds?
So when click the start button, seconds continue from 10 then to 11,12,13 and so on.
Looks like the issue is that the 10s is not added to the timeBegan. If you subtract the 10 seconds, it should work.
if (state.timeBegan === null) {
state.timeBegan = new Date() - 10_000;
}
On a side note, a mutation should be limited to just the mutation
This code is adding and removing interval from within the mutation and goes against how mutations are supposed to work.
start(state, payload) {
state.started = setInterval(() => {
payload.callback();
}, 10);
state.running = true;
},
stop(state) {
state.running = false;
state.timeStopped = new Date();
clearInterval(state.started);
},
And executing a callback from the mutation is also not good. Not that it won't work, but it's creating a weird state flow that goes against the principles of how the store works.
I want to convert a GIF Player Web Component into React Component.
I tried finding a React GIF Player library but there aren't any that are working fine.
Currently, GIF Player Web Component looks promising but it's not available in React. It looks like:
import { LitElement, html, css } from "lit";
import { GifReader } from "omggif";
class GifPlayer extends LitElement {
static get styles() {
return css`
:host {
display: inline-block;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
`;
}
static get properties() {
return {
src: { type: String },
alt: { type: String },
autoplay: { type: Boolean },
play: { type: Function },
pause: { type: Function },
restart: { type: Function },
currentFrame: { type: Number },
frames: { attribute: false, type: Array },
playing: { attribute: false, type: Boolean },
width: { attribute: false, type: Number },
height: { attribute: false, type: Number },
};
}
constructor() {
super();
this.currentFrame = 1;
this.frames = [];
this.step = this.step();
this.play = this.play.bind(this);
this.pause = this.pause.bind(this);
this.renderFrame = this.renderFrame.bind(this);
this.loadSource = this.loadSource.bind(this);
}
firstUpdated() {
this.canvas = this.renderRoot.querySelector("canvas");
this.context = this.canvas.getContext("2d");
this.loadSource(this.src).then(() => {
if (this.autoplay) this.play();
});
}
updated(changedProperties) {
if (changedProperties.has("width")) {
this.canvas.width = this.width;
this.renderFrame(false);
}
if (changedProperties.has("height")) {
this.canvas.height = this.height;
this.renderFrame(false);
}
}
render() {
return html`<canvas role="img" aria-label=${this.alt}></canvas>`;
}
play() {
if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
this.animationFrame = requestAnimationFrame(this.step);
this.playing = true;
}
pause() {
this.playing = false;
if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
}
restart() {
this.currentFrame = 1;
if (this.playing) {
this.play();
} else {
this.pause();
this.renderFrame(false);
}
}
step() {
let previousTimestamp;
return (timestamp) => {
if (!previousTimestamp) previousTimestamp = timestamp;
const delta = timestamp - previousTimestamp;
const delay = this.frames[this.currentFrame]?.delay;
if (this.playing && delay && delta > delay) {
previousTimestamp = timestamp;
this.renderFrame();
}
this.animationFrame = requestAnimationFrame(this.step);
};
}
renderFrame(progress = true) {
if (!this.frames.length) return;
if (this.currentFrame === this.frames.length - 1) {
this.currentFrame = 0;
}
this.context.putImageData(this.frames[this.currentFrame].data, 0, 0);
if (progress) {
this.currentFrame = this.currentFrame + 1;
}
}
async loadSource(url) {
const response = await fetch(url);
const buffer = await response.arrayBuffer();
const uInt8Array = new Uint8Array(buffer);
const gifReader = new GifReader(uInt8Array);
const gif = gifData(gifReader);
const { width, height, frames } = gif;
this.width = width;
this.height = height;
this.frames = frames;
if (!this.alt) {
this.alt = url;
}
this.renderFrame(false);
}
}
function gifData(gif) {
const frames = Array.from(frameDetails(gif));
return { width: gif.width, height: gif.height, frames };
}
function* frameDetails(gifReader) {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
const frameCount = gifReader.numFrames();
let previousFrame;
for (let i = 0; i < frameCount; i++) {
const frameInfo = gifReader.frameInfo(i);
const imageData = context.createImageData(
gifReader.width,
gifReader.height
);
if (i > 0 && frameInfo.disposal < 2) {
imageData.data.set(new Uint8ClampedArray(previousFrame.data.data));
}
gifReader.decodeAndBlitFrameRGBA(i, imageData.data);
previousFrame = {
data: imageData,
delay: gifReader.frameInfo(i).delay * 10,
};
yield previousFrame;
}
}
customElements.define("gif-player", GifPlayer);
However, I don't know how to convert it to a React Component.
I want to convert it in TypeScript. I've managed to convert it a little bit like:
// inspired by https://github.com/WillsonSmith/gif-player-component/blob/main/gif-player.js
import React from 'react'
import { GifReader } from 'omggif'
export class GifPlayer extends React.Component {
static get styles() {
return `
:host {
display: inline-block;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
`
}
static get properties() {
return {
src: { type: String },
alt: { type: String },
autoplay: { type: Boolean },
play: { type: Function },
pause: { type: Function },
restart: { type: Function },
currentFrame: { type: Number },
frames: { attribute: false, type: Array },
playing: { attribute: false, type: Boolean },
width: { attribute: false, type: Number },
height: { attribute: false, type: Number },
}
}
constructor(props) {
super(props)
this.currentFrame = 1
this.frames = []
this.step = this.step()
this.play = this.play.bind(this)
this.pause = this.pause.bind(this)
this.renderFrame = this.renderFrame.bind(this)
this.loadSource = this.loadSource.bind(this)
}
firstUpdated = () => {
this.canvas = this.renderRoot.querySelector('canvas')
this.context = this.canvas.getContext('2d')
this.loadSource(this.src).then(() => {
if (this.autoplay) this.play()
})
}
updated = (changedProperties) => {
if (changedProperties.has('width')) {
this.canvas.width = this.width
this.renderFrame(false)
}
if (changedProperties.has('height')) {
this.canvas.height = this.height
this.renderFrame(false)
}
}
render() {
const { alt } = this.props
return <canvas role="img" aria-label={alt}></canvas>
}
play = () => {
if (this.animationFrame) cancelAnimationFrame(this.animationFrame)
this.animationFrame = requestAnimationFrame(this.step)
this.playing = true
}
pause = () => {
this.playing = false
if (this.animationFrame) cancelAnimationFrame(this.animationFrame)
}
restart = () => {
this.currentFrame = 1
if (this.playing) {
this.play()
} else {
this.pause()
this.renderFrame(false)
}
}
step = () => {
let previousTimestamp
return (timestamp) => {
if (!previousTimestamp) previousTimestamp = timestamp
const delta = timestamp - previousTimestamp
const delay = this.frames[this.currentFrame]?.delay
if (this.playing && delay && delta > delay) {
previousTimestamp = timestamp
this.renderFrame()
}
this.animationFrame = requestAnimationFrame(this.step)
}
}
renderFrame = (progress = true) => {
if (!this.frames.length) return
if (this.currentFrame === this.frames.length - 1) {
this.currentFrame = 0
}
this.context.putImageData(this.frames[this.currentFrame].data, 0, 0)
if (progress) {
this.currentFrame = this.currentFrame + 1
}
}
loadSource = async (url) => {
const response = await fetch(url)
const buffer = await response.arrayBuffer()
const uInt8Array = new Uint8Array(buffer)
const gifReader = new GifReader(uInt8Array)
const gif = gifData(gifReader)
const { width, height, frames } = gif
this.width = width
this.height = height
this.frames = frames
if (!this.alt) {
this.alt = url
}
this.renderFrame(false)
}
}
function gifData(gif) {
const frames = Array.from(frameDetails(gif))
return { width: gif.width, height: gif.height, frames }
}
function* frameDetails(gifReader) {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
if (!context) return
const frameCount = gifReader.numFrames()
let previousFrame
for (let i = 0; i < frameCount; i++) {
const frameInfo = gifReader.frameInfo(i)
const imageData = context.createImageData(gifReader.width, gifReader.height)
if (i > 0 && frameInfo.disposal < 2) {
imageData.data.set(new Uint8ClampedArray(previousFrame.data.data))
}
gifReader.decodeAndBlitFrameRGBA(i, imageData.data)
previousFrame = {
data: imageData,
delay: gifReader.frameInfo(i).delay * 10,
}
yield previousFrame
}
}
However, I'm getting all kinds of TypeScript errors. I also don't know how to style :host css property.
How can I solve it?
Quick link for people who don't want to read this wall of text: repository link
This was a pretty fun project. I'm not guaranteeing that I support all use-cases, but here is a modern-day implementation that takes advantage of TypeScript. It also prefers the more modern and composable React Hook API over using class components.
The basic idea is that you have useGifController hook, which is linked to a canvas via a ref. This controller then allows you to handle loading and error states on your own, and then control the GIF rendered in a canvas as needed. So for example, we could write a GifPlayer component as follows:
GifPlayer.tsx
import React, { useRef } from 'react'
import { useGifController } from '../hooks/useGifController'
export function GifPlayer(): JSX.Element | null {
const canvasRef = useRef<HTMLCanvasElement>(null)
const gifController = useGifController('/cradle.gif', canvasRef, true)
if (gifController.loading) {
return null
}
if (gifController.error) {
return null
}
const { playing, play, pause, restart, renderNextFrame, renderPreviousFrame, width, height } = gifController
return (
<div>
<canvas {...gifController.canvasProps} ref={canvasRef} />
<div style={{ display: 'flex', gap: 16, justifyContent: 'space-around' }}>
<button onClick={renderPreviousFrame}>Previous</button>
{playing ? <button onClick={pause}>Pause</button> : <button onClick={play}>Play</button>}
<button onClick={restart}>Restart</button>
<button onClick={renderNextFrame}>Next</button>
</div>
<div>
<p>Width: {width}</p>
<p>Height: {height}</p>
</div>
</div>
)
}
Here, you can see the gifController expects a URL containing the GIF, and a ref to the canvas element. Then, once you handle the loading and error states, you have access to all of the controls GifController provides. play, pause, renderNextFrame, renderPreviousFrame all do exactly what you would expect.
So, what's inside of this useGifController hook? Well... it's a bit lengthy, but hopefully I've documented this well-enough so you can understand after studying it for a bit.
useGifController.ts
import { GifReader } from 'omggif'
import {
RefObject,
DetailedHTMLProps,
CanvasHTMLAttributes,
useEffect,
useState,
MutableRefObject,
useRef,
} from 'react'
import { extractFrames, Frame } from '../lib/extractFrames'
type HTMLCanvasElementProps = DetailedHTMLProps<CanvasHTMLAttributes<HTMLCanvasElement>, HTMLCanvasElement>
type GifControllerLoading = {
canvasProps: HTMLCanvasElementProps
loading: true
error: false
}
type GifControllerError = {
canvasProps: HTMLCanvasElementProps
loading: false
error: true
errorMessage: string
}
type GifControllerResolved = {
canvasProps: HTMLCanvasElementProps
loading: false
error: false
frameIndex: MutableRefObject<number>
playing: boolean
play: () => void
pause: () => void
restart: () => void
renderFrame: (frame: number) => void
renderNextFrame: () => void
renderPreviousFrame: () => void
width: number
height: number
}
type GifController = GifControllerLoading | GifControllerResolved | GifControllerError
export function useGifController(
url: string,
canvas: RefObject<HTMLCanvasElement | null>,
autoplay = false,
): GifController {
type LoadingState = {
loading: true
error: false
}
type ErrorState = {
loading: false
error: true
errorMessage: string
}
type ResolvedState = {
loading: false
error: false
gifReader: GifReader
frames: Frame[]
}
type State = LoadingState | ResolvedState | ErrorState
const ctx = canvas.current?.getContext('2d')
// asynchronous state variables strongly typed as a union such that properties
// are only defined when `loading === true`.
const [state, setState] = useState<State>({ loading: true, error: false })
const [shouldUpdate, setShouldUpdate] = useState(false)
const [canvasAccessible, setCanvasAccessible] = useState(false)
const frameIndex = useRef(-1)
// state variable returned by hook
const [playing, setPlaying] = useState(false)
// ref that is used internally
const _playing = useRef(false)
// Load GIF on initial render and when url changes.
useEffect(() => {
async function loadGif() {
const response = await fetch(url)
const buffer = await response.arrayBuffer()
const uInt8Array = new Uint8Array(buffer)
// Type cast is necessary because GifReader expects Buffer, which extends
// Uint8Array. Doesn't *seem* to cause any runtime errors, but I'm sure
// there's some edge case I'm not covering here.
const gifReader = new GifReader(uInt8Array as Buffer)
const frames = extractFrames(gifReader)
if (!frames) {
setState({ loading: false, error: true, errorMessage: 'Could not extract frames from GIF.' })
} else {
setState({ loading: false, error: false, gifReader, frames })
}
// must trigger re-render to ensure access to canvas ref
setShouldUpdate(true)
}
loadGif()
// only run this effect on initial render and when URL changes.
// eslint-disable-next-line
}, [url])
// update if shouldUpdate gets set to true
useEffect(() => {
if (shouldUpdate) {
setShouldUpdate(false)
} else if (canvas.current !== null) {
setCanvasAccessible(true)
}
}, [canvas, shouldUpdate])
// if canvasAccessible is set to true, render first frame and then autoplay if
// specified in hook arguments
useEffect(() => {
if (canvasAccessible && frameIndex.current === -1) {
renderNextFrame()
autoplay && setPlaying(true)
}
// ignore renderNextFrame as it is referentially unstable
// eslint-disable-next-line
}, [canvasAccessible])
useEffect(() => {
if (playing) {
_playing.current = true
_iterateRenderLoop()
} else {
_playing.current = false
}
// ignore _iterateRenderLoop() as it is referentially unstable
// eslint-disable-next-line
}, [playing])
if (state.loading === true || !canvas) return { canvasProps: { hidden: true }, loading: true, error: false }
if (state.error === true)
return { canvasProps: { hidden: true }, loading: false, error: true, errorMessage: state.errorMessage }
const { width, height } = state.gifReader
return {
canvasProps: { width, height },
loading: false,
error: false,
playing,
play,
pause,
restart,
frameIndex,
renderFrame,
renderNextFrame,
renderPreviousFrame,
width,
height,
}
function play() {
if (state.error || state.loading) return
if (playing) return
setPlaying(true)
}
function _iterateRenderLoop() {
if (state.error || state.loading || !_playing.current) return
const delay = state.frames[frameIndex.current].delay
setTimeout(() => {
renderNextFrame()
_iterateRenderLoop()
}, delay)
}
function pause() {
setPlaying(false)
}
function restart() {
frameIndex.current = 0
setPlaying(true)
}
function renderFrame(frameIndex: number) {
if (!ctx || state.loading === true || state.error === true) return
if (frameIndex < 0 || frameIndex >= state.gifReader.numFrames()) return
ctx.putImageData(state.frames[frameIndex].imageData, 0, 0)
}
function renderNextFrame() {
if (!ctx || state.loading === true || state.error === true) return
const nextFrame = frameIndex.current + 1 >= state.gifReader.numFrames() ? 0 : frameIndex.current + 1
renderFrame(nextFrame)
frameIndex.current = nextFrame
}
function renderPreviousFrame() {
if (!ctx || state.loading === true || state.error === true) return
const prevFrame = frameIndex.current - 1 < 0 ? state.gifReader.numFrames() - 1 : frameIndex.current - 1
renderFrame(prevFrame)
frameIndex.current = prevFrame
}
}
This in turn depends on extractFrames, which extracts the frames as ImageData objects along with their respective delays.
extractFrames.ts
import { GifReader } from 'omggif'
export type Frame = {
/**
* A full frame of a GIF represented as an ImageData object. This can be
* rendered onto a canvas context simply by calling
* `ctx.putImageData(frame.imageData, 0, 0)`.
*/
imageData: ImageData
/**
* Delay in milliseconds.
*/
delay: number
}
/**
* Function that accepts a `GifReader` instance and returns an array of
* `ImageData` objects that represent the frames of the gif.
*
* #param gifReader The `GifReader` instance.
* #returns An array of `ImageData` objects representing each frame of the GIF.
* Or `null` if something went wrong.
*/
export function extractFrames(gifReader: GifReader): Frame[] | null {
const frames: Frame[] = []
// the width and height of the complete gif
const { width, height } = gifReader
// This is the primary canvas that the tempCanvas below renders on top of. The
// reason for this is that each frame stored internally inside the GIF is a
// "diff" from the previous frame. To resolve frame 4, we must first resolve
// frames 1, 2, 3, and then render frame 4 on top. This canvas accumulates the
// previous frames.
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
if (!ctx) return null
for (let frameIndex = 0; frameIndex < gifReader.numFrames(); frameIndex++) {
// the width, height, x, and y of the "dirty" pixels that should be redrawn
const { width: dirtyWidth, height: dirtyHeight, x: dirtyX, y: dirtyY, disposal, delay } = gifReader.frameInfo(0)
// skip this frame if disposal >= 2; from GIF spec
if (disposal >= 2) continue
// create hidden temporary canvas that exists only to render the "diff"
// between the previous frame and the current frame
const tempCanvas = document.createElement('canvas')
tempCanvas.width = width
tempCanvas.height = height
const tempCtx = tempCanvas.getContext('2d')
if (!tempCtx) return null
// extract GIF frame data to tempCanvas
const newImageData = tempCtx.createImageData(width, height)
gifReader.decodeAndBlitFrameRGBA(frameIndex, newImageData.data)
tempCtx.putImageData(newImageData, 0, 0, dirtyX, dirtyY, dirtyWidth, dirtyHeight)
// draw the tempCanvas on top. ctx.putImageData(tempCtx.getImageData(...))
// is too primitive here, since the pixels would be *replaced* by incoming
// RGBA values instead of layered.
ctx.drawImage(tempCanvas, 0, 0)
frames.push({
delay: delay * 10,
imageData: ctx.getImageData(0, 0, width, height),
})
}
return frames
}
I do plan on making this an NPM package someday. I think it'd be pretty useful.
export default {
data() {
return {
channels: channelz.Root.Channels.Channel,
currentIndex: 1,
columnNumber: 9,
rowNumber: 7,
windows: {
width: 0,
height: 0
}
};
},
mounted() {
window.addEventListener('resize', this.handleResize);
window.innerWidth < 1200 ? this.handleResize() : console.log("Window size hasn't changed!");
},
destroyed() {
window.removeEventListener('resize', this.handleResize);
},
methods: {
handleResize() {
this.windows.width = window.innerWidth;
this.windows.height = window.innerHeight;
this.checkAndChangeColumnRow();
//this.checkRowNumber();
},
checkAndChangeColumnRow() {
var calcCol = 0;
for (var i = 1; i < this.channels.length; i++) {
calcCol++;
if (this.$refs[i][0]['y'] != this.$refs[i + 1][0]['y']) {
break;
}
}
this.columnNumber = this.columnNumber !== calcCol ? calcCol : this.columnNumber;
this.rowNumber = (this.channels.length - 1) % this.columnNumber == 0 ? parseInt((this.channels.length - 1) / this.columnNumber) : parseInt(this.channels.length / this.columnNumber) + 1;
}
}
}
I am trying to manipulate and find column and row number of the items in the flexbox. I am trying to find it out by the properties of this.$refs properties as you can see in the code above. Unfortunately, checkAndChangeColumnRow function updates it earlier than items gets completely rendered.
Edit: Resolved the issue by adding an event listener in mounted. It calls the same function twice in a row and it somewhat gives the correct output in the second callback.
mounted() {
window.addEventListener('resize', this.handleResize);
window.addEventListener('load',this.checkAndChangeColumnRow);
window.innerWidth < 1200 ? (console.log("Width çok azmış yaa"),this.handleResize()) : console.log("Window size hasn't changed!");
},
destroyed() {
window.removeEventListener('resize', this.handleResize);
window.removeEventListener('load',(this.checkAndChangeColumnRow,console.log("Event has been detached.")));
},
I have a vue component responsible for pagination, so that only 20 items are shown per page. Everything is working except for the one value that I need passed down from the parent component, the 'count' value that returns the total number of items.
I use the 'count' value to calculate whether the "Next Page" button is active or not and what the total number of pages should be. Using console.log I can see that the 'count' value returns correctly in to the parent component, but it always comes back as undefined in the child pagination component. Please advise how I can fix this.
(I'm not sure if using watch here is correct; I thought it would solve the problem, but it changed nothing.)
EDIT: I've posted more of both the child and parent component for better context.
Here is the script of the child component:
<script>
export default {
name: 'Pagination',
props: ['count'],
watch: {
count: function (val, oldVal) {}
},
data () {
return {
currentPage: 1,
limit: 20,
paginationVisible: true,
firstPageIsCurrent: true,
prevEllipsisVisible: false,
pageLink1: 2,
pageLink1Visible: true,
pageLink1IsCurrent: false,
pageLink2: 3,
pageLink2Visible: true,
pageLink2IsCurrent: false,
pageLink3: 4,
pageLink3Visible: true,
pageLink3IsCurrent: false,
nextEllipsisVisible: true,
lastPageVisible: true,
lastPageIsCurrent: false,
prevDisabled: true,
nextDisabled: false,
lastPage: 1
}
},
methods: {
handlePrev () {
if (!this.prevDisabled) {
this.currentPage -= 1
this.paginate()
}
},
handleNext () {
if ((this.currentPage * this.limit) < this.count) {
this.currentPage += 1
this.paginate()
}
},
handleFirstPage () {
if (this.currentPage !== 1) {
this.currentPage = 1
this.paginate()
}
},
handlePageLink1 () {
if (this.currentPage !== this.pageLink1) {
this.currentPage = this.pageLink1
this.paginate()
}
},
handlePageLink2 () {
if (this.currentPage !== this.pageLink2) {
this.currentPage = this.pageLink2
this.paginate()
}
},
handlePageLink3 () {
if (this.currentPage !== this.pageLink3) {
this.currentPage = this.pageLink3
this.paginate()
}
},
handleLastPage () {
if (this.currentPage < this.lastPage) {
this.currentPage = this.lastPage
this.paginate()
}
},
paginateAdjust () {
console.log(this.count)
// adjust pagination bar and previous/next buttons
this.nextDisabled = ((this.currentPage * this.limit) >= this.count)
this.prevDisabled = (this.currentPage === 1)
const pageCount = Math.ceil(this.count / this.limit)
// console.log(pageCount + ', cp: ' + this.currentPage)
if (pageCount === 1) {
this.paginationVisible = false
} else {
this.paginationVisible = true
// first page link
this.firstPageIsCurrent = this.currentPage === 1
// previous ellipsis
this.prevEllipsisVisible = this.currentPage > 3 && pageCount > 5
// first page link
this.pageLink2Visible = pageCount > 2
if (this.currentPage < 4) {
this.pageLink1 = 2
} else if ((pageCount - this.currentPage) < 3) {
this.pageLink1 = pageCount - 3
} else {
this.pageLink1 = this.currentPage - 1
}
this.pageLink1IsCurrent = this.pageLink1 === this.currentPage
// second page link
this.pageLink2Visible = pageCount > 3
if (this.currentPage < 4) {
this.pageLink2 = 3
} else if ((pageCount - this.currentPage) < 3) {
this.pageLink2 = pageCount - 2
} else {
this.pageLink2 = this.currentPage
}
this.pageLink2IsCurrent = this.pageLink2 === this.currentPage
// third page link
this.pageLink3Visible = pageCount > 4
if (this.currentPage < 4) {
this.pageLink3 = 4
} else if ((pageCount - this.currentPage) < 3) {
this.pageLink3 = pageCount - 1
} else {
this.pageLink3 = this.currentPage + 1
}
this.pageLink3IsCurrent = this.pageLink3 === this.currentPage
// next ellipsis
this.nextEllipsisVisible = ((pageCount - this.currentPage) >= 3) && pageCount > 5
// last page
this.lastPage = pageCount
this.lastPageIsCurrent = this.currentPage === pageCount
}
},
emitMSQ () {
this.$emit('msq', this.currentPage, this.limit)
},
paginate () {
this.paginateAdjust()
this.emitMSQ()
}
},
created () {
this.paginate()
}
}
</script>
Here is the (relevant) script from the parent component:
export default {
name: 'index',
components: {Pagination},
computed: {
apps () {
return this.$store.state.apps
}
},
data () {
return {
apps: [],
count: 0,
searchAddress: ''
}
},
methods: {
onSearch () {
if (this.searchAddress.length === 12 || this.searchAddress.length === 0) {
this.currentPage = 1
this.makeServerQuery()
}
},
makeServerQuery (cp, pp) {
let queryStr = '?uid=' + this.$auth.user().id + '&cp=' + cp + '&pp=' + pp
if (this.searchAddress.length === 12) {
queryStr += '&se=' + this.searchAddress
}
this.$http.get('/apps' + queryStr).then(res => {
this.apps = res.data.apps
this.count = res.data.count
console.log(this.count + ' msq()')
})
},
The emit, server query, and actual pagination elements all work fine. But my next page button is wrong and the last page button displays NaN because it can't run the correct calculations when count === undefined.
Thanks in advance for the help.
From your comments, it looks like you are not really passing anything to your component's prop.
As #Dan suggested, your count prop is undefined because nothing is being passed to it from the parent. To pass the count data property from the parent to the child you need to use a template binding <child v-bind:count="count"></child> or its shorthand <child :count="count"></child>.
The official documentation is a good starting point for understanding props.
Here's a minimal example.
Vue.config.productionTip = false;
Vue.component('child', {
template: `
<div class="child">
from parent: {{count}}
<br>
<button :disabled="!enableNext" #click="onNext">Next</button>
</div>
`,
props: ['count'],
computed: {
enableNext() {
return this.count && this.count > 0;
}
},
methods: {
onNext() {
console.log('Go to page ' + this.count + 1)
}
}
})
new Vue({
el: '#app',
template: `
<div class="parent">
Parent count: {{ count }}
<child :count="count" />
<br>
<button #click="count++">Increment count</button>
</div>
`,
data:() => ({
count: 0
})
});
div { border: solid 1px black; padding: 1em; }
.child { background: lightgray; }
button:disabled, button[disabled]{
border: 1px solid #999999;
background-color: #cccccc;
color: #666666;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>