PDFJS: Rendering PDF on canvas - javascript

I am trying to render pdf file on canvas.
Framework is Nuxt.js (Vue)
Finally, I rendered pdf on canvas, but something is strange. Zoomed pdf was rendered.
I tried to change scale and other values, but the result was same.
How can I fix this?
Please check my attached images.
This is excepted pdf.
And my result is as following: (Current scale value is 1.5)
This is my some code to render pdf.
<script>
export default {
props: ["page", "scale"],
mounted() {
this.drawPage();
},
beforeDestroy() {
this.destroyPage(this.page);
},
methods: {
drawPage() {
if (this.renderTask) return;
const { viewport } = this;
const canvasContext = this.$el.getContext("2d");
const renderContext = { canvasContext, viewport };
this.renderTask = this.page.render(renderContext);
this.renderTask.promise.then(() => this.$emit("rendered", this.page));
this.renderTask.promise.then(/* */).catch(this.destroyRenderTask);
},
destroyPage(page) {
if (!page) return;
page._destroy();
if (this.renderTask) this.renderTask.cancel();
},
destroyRenderTask() {
if (!this.renderTask) return;
this.renderTask.cancel();
delete this.renderTask;
},
},
render(h) {
const { canvasAttrs: attrs } = this;
return h("canvas", { attrs });
},
created() {
this.viewport = this.page.getViewport({
scale: this.scale,
});
},
computed: {
canvasAttrs() {
let { width, height } = this.viewport;
[width, height] = [width, height].map((dim) => Math.ceil(dim));
const style = this.canvasStyle;
return {
style,
class: "pdf-page",
};
},
canvasStyle() {
const {
width: actualSizeWidth,
height: actualSizeHeight,
} = this.actualSizeViewport;
const pixelRatio = window.devicePixelRatio || 1;
const [pixelWidth, pixelHeight] = [
actualSizeWidth,
actualSizeHeight,
].map((dim) => Math.ceil(dim / pixelRatio));
return `width: ${pixelWidth}px; height: ${pixelHeight}px;`;
},
actualSizeViewport() {
return this.viewport.clone({ scale: this.scale });
},
},
watch: {
page(page, oldPage) {
this.destroyPage(oldPage);
},
},
};
</script>

Related

A-FRAME Modifying a model's materials' stencil properties breaks rendering

I am experimenting with the stencil buffer and have created two components. The first is applied to an entity and is supposed to draw into the stencil buffer:
AFRAME.registerComponent("stencil-surface", {
dependencies: ["mesh"],
schema: {
mask: { default: 1 },
order: { default: 1 },
},
updateMaterials: function (mesh) {
const self = this;
mesh.traverse(function (node) {
mesh.renderOrder = self.data.order;
if (node.material) {
node.material.colorWrite = false;
node.material.depthWrite = false;
node.material.stencilWrite = true;
node.material.stencilRef = self.data.mask;
node.material.stencilFuncMask = 0xFF;
node.material.stencilFunc= THREE.AlwaysStencilFunc;
node.material.stencilFail= THREE.ReplaceStencilOp;
node.material.stencilZFail= THREE.ReplaceStencilOp;
node.material.stencilZPass= THREE.ReplaceStencilOp;
}
});
},
init: function () {
const self = this;
const el = this.el;
const mesh = el.getObject3D("mesh");
if (mesh) {
el.object3D.renderOrder = self.data.order;
self.updateMaterials(mesh);
} else {
el.addEventListener("model-loaded", function () {
el.object3D.renderOrder = self.data.order;
const mesh = el.getObject3D("mesh");
if (mesh) {
self.updateMaterials(mesh);
}
});
}
},
});
The second is applied to other entities to get them to only draw in the area marked by that stencil mask (and set the renderOrder so they draw afterwards):
AFRAME.registerComponent("stencil-member", {
dependencies: ["mesh"],
schema: {
mask: { default: 1 },
order: { default: 2 },
},
updateMaterials: function (mesh) {
const self = this;
mesh.traverse(function (node) {
mesh.renderOrder = self.data.order;
if (node.material) {
node.material.colorWrite = true;
node.material.depthWrite = true;
node.material.stencilWrite = true;
node.material.stencilRef = self.data.mask;
node.material.stencilFuncMask = 0xFF;
node.material.stencilFunc = THREE.EqualStencilFunc;
node.material.stencilFail = THREE.KeepStencilOp;
node.material.stencilZFail = THREE.KeepStencilOp;
node.material.stencilZPass = THREE.ReplaceStencilOp;
}
});
},
init: function () {
const self = this;
const el = this.el;
const mesh = el.getObject3D("mesh");
if (mesh) {
el.object3D.renderOrder = this.data.order;
self.updateMaterials(mesh);
} else {
el.addEventListener("model-loaded", function () {
el.object3D.renderOrder = self.data.order;
const mesh = el.getObject3D("mesh");
if (mesh) {
self.updateMaterials(mesh);
}
});
}
},
});
This works if I add the stencil-member component to a shape:
<a-plane
id="stencil"
stencil-surface="mask: 1"
>
<a-box
id="member"
position="0 0 -0.25"
scale="0.5 2 1"
color="#AAFFFF"
stencil-member="mask: 1"
></a-box>
</a-plane>
But I get weird results when I apply it to a model inside the same plane:
<a-assets>
<a-asset-item
id="model1"
src="https://cdn.glitch.global/ed7502f7-8600-47d9-8c66-96d695a11101/RobotExpressive.glb?v=1653017847384"
></a-asset-item>
</a-assets>
<a-entity
id="member2"
gltf-model="#model1"
position="0 0 -0.25"
scale="0.1 0.1 0.1"
stencil-member="mask: 1"
></a-entity>
With this, only the arms and the legs of the model (taken from https://threejs.org/examples/#webgl_animation_skinning_morph) render. Any ideas why this doesn't work? Is there something weird about the model? Is it an issue with the render ordering? Thank you!

Convert GIF Player Web Component into React Component?

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.

How can I stop the current swipe on a trackpad

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
}
}

Tiptap extension for images and editable captions

enter image description here
I want to implement image and editable caption with tiptap extension
https://discuss.prosemirror.net/t/figure-and-editable-caption/462
There was a very good example with ProseMirror, but is it difficult to achieve with tiptap?
Please tell me what code you should write if possible.
I attach the code that I wrote below.
The image and caption have been successfully added, but the caption cannot be edited yet.
// ./CustomImage.ts
// #ts-ignore
import { Node, Plugin } from 'tiptap'
// #ts-ignore
import { nodeInputRule } from 'tiptap-commands'
const IMAGE_INPUT_REGEX = /!\[(.+|:?)\]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/
export default class CustomImage extends Node {
get name () {
return 'customImage'
}
get schema () {
return {
attrs: {
src: {
default: null
},
alt: {
default: null
},
title: {
default: null
},
caption: {
default: null
}
},
group: 'block',
selectable: false,
draggable: true,
parseDOM: [
{
tag: 'figure'
},
[
{
tag: 'img[src]',
getAttrs: (dom: any) => ({
src: dom.getAttribute('src'),
title: dom.getAttribute('title'),
alt: dom.getAttribute('alt')
})
},
{
tag: 'figcaption'
}
]
],
toDOM: (node: any) => [
'figure',
[
'img',
{
src: node.attrs.src,
title: node.attrs.title,
alt: node.attrs.alt
}
],
[
'figcaption',
{
contenteditable: 'true'
},
node.attrs.caption
]
]
}
}
commands ({ type }: any) {
return (attrs: any) => (state: any, dispatch: any) => {
const { selection } = state
const position = selection.$cursor ? selection.$cursor.pos : selection.$to.pos
const node = type.create(attrs)
const transaction = state.tr.insert(position, node)
dispatch(transaction)
}
}
inputRules ({ type }: any) {
return [
nodeInputRule(IMAGE_INPUT_REGEX, type, (match: any) => {
const [, alt, src, title] = match
return {
src,
alt,
title
}
})
]
}
get plugins () {
return [
new Plugin({
props: {
handleDOMEvents: {
drop (view: any, event: any) {
const hasFiles = event.dataTransfer &&
event.dataTransfer.files &&
event.dataTransfer.files.length
if (!hasFiles) {
return
}
const images = Array
.from(event.dataTransfer.files)
.filter((file: any) => (/image/i).test(file.type))
if (images.length === 0) {
return
}
event.preventDefault()
const { schema } = view.state
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY })
images.forEach((image: any) => {
const reader = new FileReader()
reader.onload = (readerEvent: any) => {
const node = schema.nodes.image.create({
src: readerEvent.target.result
})
const transaction = view.state.tr.insert(coordinates.pos, node)
view.dispatch(transaction)
}
reader.readAsDataURL(image)
})
}
}
}
})
]
}
}
You're missing a "hole" in the figure caption to make it editable. See https://prosemirror.net/docs/ref/#model.NodeSpec.toDOM. I implemented one of the solutions mentioned in the ProseMirror thread mentioned; however, the cursor can still enter text in between image and figurecaption. I would recommend using the last example with 3 schemas instead.
get schema() {
return {
content: "inline*",
attrs: {src: {default: ""}, title: {default: ""}},
group: 'block',
draggable: true,
isolating: true,
parseDOM: [{
tag: "figure",
contentElement: "figcaption",
getAttrs(dom) {
let img = dom.querySelector("img")
console.log(img, img.getAttribute('src'), img.getAttribute('alt'));
return {src: img.getAttribute('src'), title: img.getAttribute('alt')}
}
}],
toDOM: node => ["figure", ["img", node.attrs], ["figurecaption", 0]],
}
}

Vue Watch doesnt Get triggered when using axios

Hey guys I have this code that fetches data from database usin axios, and in the .then() function I set a data property, watch doesnt trigger. Here is some code that I currently have. And thank you in advance!
export default {
name: '..',
data() {
return {
autocompleteOn: false
}
},
watch: {
autocompleteOn(oldVal, newVal) {
console.log('autocomplet') // doesnt trigger this
}
},
methods: {
fetchAutocompleteResults: _.debounce((filter) => {
let $this = this;
let data = {
filter: filter,
page: $this.page
};
filter.resources.response = [];
filter.loading = true;
axios.post(BASE_URL + '/search/filter', data).then(function(response) {
if (response.data.length) {
filter.autocompleteOn = true;
$this.autocompleteOn = true;
filter.resources.response = filter.resources.response.concat(response.data);
$this.currentFilter = filter;
$this.page++;
console.log($this.autocompleteOn); // this is correct
}
filter.loading = false;
});
}, 300)
}
}
The debounce with an arrow function is making the this be something other than the Vue instance (e.g. window).
Instead of:
methods: {
fetchAutocompleteResults: _.debounce((filter) => {
Use:
methods: {
fetchAutocompleteResults: _.debounce(function (filter) {
// ^^^^^^^^ ^^^
Demo:
new Vue({
el: '#app',
data() {
return {
autocompleteOn: false
}
},
watch: {
autocompleteOn(oldVal, newVal) {
console.log('autocomplet') // doesnt trigger this
}
},
methods: {
fetchAutocompleteResults: _.debounce(function (filter) { // CHANGED from arrow function
let $this = this;
let data = {
filter: filter,
page: $this.page
};
filter.resources.response = [];
filter.loading = true;
// changed data for demo
data = [{title: 'foo', body: 'bar', userId: 1}];
// changed URL for demo
axios.post('https://jsonplaceholder.typicode.com/posts', data).then(function(response) {
if (response.data.length) {
filter.autocompleteOn = true;
$this.autocompleteOn = true;
filter.resources.response = filter.resources.response.concat(response.data);
$this.currentFilter = filter;
$this.page++;
console.log($this.autocompleteOn); // this is correct
}
filter.loading = false;
});
}, 300)
}
})
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.5/lodash.min.js"></script>
<div id="app">
<button #click="fetchAutocompleteResults({resources: {}})">fetchAutocompleteResults</button>
</div>

Categories

Resources