Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"scripts": {
"build": "rete build -c rete.config.ts",
"doc": "rete doc",
"lint": "rete lint"
"lint": "rete lint",
"test": "rete test"
},
"author": "Vitaliy Stoliarov",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface ConnectionView {
interface Content {
holder: HTMLElement
add(element: HTMLElement): void
// eslint-disable-next-line no-undef

reorder(target: HTMLElement, next: ChildNode | null): void
remove(element: HTMLElement): void
}
Expand Down
4 changes: 3 additions & 1 deletion src/connection-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export class ConnectionView {
this.element.style.position = 'absolute'
this.element.style.left = '0'
this.element.style.top = '0'
this.element.addEventListener('contextmenu', event => events.contextmenu(event))
this.element.addEventListener('contextmenu', event => {
events.contextmenu(event)
})
}
}
1 change: 0 additions & 1 deletion src/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export class Content {
this.holder.appendChild(element)
}

// eslint-disable-next-line no-undef
async reorder(target: HTMLElement, next: ChildNode | null) {
if (!this.holder.contains(target)) {
throw new Error(`content doesn't have 'target' for reordering`)
Expand Down
1 change: 0 additions & 1 deletion src/extensions/restrictor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export function restrictor<Schemes extends BaseSchemes, K>(plugin: AreaPlugin<Sc
return zoom
}

// eslint-disable-next-line max-statements
function restrictPosition(position: Position): Position {
if (!translation) throw new Error('translation param isnt defined')
const nextPosition = { ...position }
Expand Down
2 changes: 1 addition & 1 deletion src/extensions/zoom-at.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type Params = {
* @param nodes The nodes to fit
* @param params The zoom parameters
*/
// eslint-disable-next-line max-statements, max-len
// eslint-disable-next-line max-len
export async function zoomAt<Schemes extends SchemesWithSizes, K>(plugin: AreaPlugin<Schemes, K>, nodes: NodeRef<Schemes>[], params?: Params) {
const { scale = 0.9 } = params || {}
const editor = plugin.parentScope<NodeEditor<Schemes>>(NodeEditor)
Expand Down
8 changes: 6 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,12 @@ export class AreaPlugin<Schemes extends BaseSchemes, ExtraSignals = never> exten
*/
public destroy() {
this.container.removeEventListener('contextmenu', this.onContextMenu)
Array.from(this.connectionViews.keys()).forEach(id => this.removeConnectionView(id))
Array.from(this.nodeViews.keys()).forEach(id => this.removeNodeView(id))
Array.from(this.connectionViews.keys()).forEach(id => {
this.removeConnectionView(id)
})
Array.from(this.nodeViews.keys()).forEach(id => {
this.removeNodeView(id)
})
this.area.destroy()
}
}
4 changes: 3 additions & 1 deletion src/node-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ export class NodeView {
this.position = { x: 0, y: 0 }
void this.translate(0, 0)

this.element.addEventListener('contextmenu', event => this.events.contextmenu(event))
this.element.addEventListener('contextmenu', event => {
this.events.contextmenu(event)
})

this.dragHandler = new Drag()
this.dragHandler.initialize(
Expand Down
32 changes: 32 additions & 0 deletions src/zoom-wheel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/** Pixels per wheel line ({@link WheelEvent.DOM_DELTA_LINE}) */
export const LINE_HEIGHT_PX = 8

/** Pixels per wheel page ({@link WheelEvent.DOM_DELTA_PAGE}) */
export const PAGE_HEIGHT_PX = 24

const DOM_DELTA_LINE = 1
const DOM_DELTA_PAGE = 2

/**
* Converts wheel delta to pixels based on {@link WheelEvent.deltaMode}.
* @internal
*/
export function wheelDeltaToPixels(delta: number, mode: number): number {
if (mode === DOM_DELTA_LINE) return delta * LINE_HEIGHT_PX
if (mode === DOM_DELTA_PAGE) return delta * PAGE_HEIGHT_PX

return delta
}

/**
* Maps normalized wheel movement to a zoom delta, capped at `intensity`.
* @internal
*/
export function computeWheelZoomDelta(pixels: number, intensity: number): number {
if (pixels === 0) return 0

const unit = intensity / LINE_HEIGHT_PX
const raw = -pixels * unit

return Math.sign(raw) * Math.min(intensity, Math.abs(raw))
}
7 changes: 6 additions & 1 deletion src/zoom.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { computeWheelZoomDelta, wheelDeltaToPixels } from './zoom-wheel'

/**
* Zoom source
Expand Down Expand Up @@ -35,8 +36,12 @@ export class Zoom {
protected wheel = (e: WheelEvent) => {
e.preventDefault()

const pixels = wheelDeltaToPixels(e.deltaY, e.deltaMode)
const delta = computeWheelZoomDelta(pixels, this.intensity)

if (delta === 0) return

const { left, top } = this.element.getBoundingClientRect()
const delta = -this.intensity * Math.sign(e.deltaY);
const ox = (left - e.clientX) * delta
const oy = (top - e.clientY) * delta

Expand Down
94 changes: 94 additions & 0 deletions test/zoom-wheel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {
computeWheelZoomDelta,
LINE_HEIGHT_PX,
PAGE_HEIGHT_PX,
wheelDeltaToPixels
} from '../src/zoom-wheel'

const intensity = 0.1

const DOM_DELTA_PIXEL = 0
const DOM_DELTA_LINE = 1
const DOM_DELTA_PAGE = 2

function signBasedWheelDelta(deltaY: number): number {
if (deltaY === 0) return 0

return -intensity * Math.sign(deltaY)
}

function wheelZoomDelta(deltaY: number, deltaMode: number): number {
const pixels = wheelDeltaToPixels(deltaY, deltaMode)

return computeWheelZoomDelta(pixels, intensity)
}

describe('wheelDeltaToPixels', () => {
it('returns delta unchanged in DOM_DELTA_PIXEL mode', () => {
expect(wheelDeltaToPixels(120, DOM_DELTA_PIXEL)).toBe(120)
expect(wheelDeltaToPixels(0.05, DOM_DELTA_PIXEL)).toBe(0.05)
expect(wheelDeltaToPixels(-3, DOM_DELTA_PIXEL)).toBe(-3)
})

it('multiplies by line height in DOM_DELTA_LINE mode', () => {
expect(wheelDeltaToPixels(1, DOM_DELTA_LINE)).toBe(LINE_HEIGHT_PX)
expect(wheelDeltaToPixels(-2, DOM_DELTA_LINE)).toBe(-2 * LINE_HEIGHT_PX)
})

it('multiplies by page height in DOM_DELTA_PAGE mode', () => {
expect(wheelDeltaToPixels(1, DOM_DELTA_PAGE)).toBe(PAGE_HEIGHT_PX)
expect(wheelDeltaToPixels(-1, DOM_DELTA_PAGE)).toBe(-PAGE_HEIGHT_PX)
})
})

describe('mouse-like wheel zoom', () => {
const cases = [
{ name: 'Chrome wheel down', deltaY: 120, deltaMode: DOM_DELTA_PIXEL, expected: -intensity },
{ name: 'Chrome wheel up', deltaY: -120, deltaMode: DOM_DELTA_PIXEL, expected: intensity },
{ name: 'Firefox wheel down', deltaY: 1, deltaMode: DOM_DELTA_LINE, expected: -intensity },
{ name: 'Firefox wheel up', deltaY: -1, deltaMode: DOM_DELTA_LINE, expected: intensity },
{ name: 'page scroll down', deltaY: 1, deltaMode: DOM_DELTA_PAGE, expected: -intensity }
]

cases.forEach(({ name, deltaY, deltaMode, expected }) => {
it(`${name} matches sign-based delta`, () => {
const delta = wheelZoomDelta(deltaY, deltaMode)

expect(delta).toBe(expected)
expect(delta).toBe(signBasedWheelDelta(deltaY))
})
})
})

describe('touchpad wheel zoom', () => {
const cases = [
{ name: 'pinch light', deltaY: 0.05, expected: -0.000625 },
{ name: 'pinch medium', deltaY: 0.5, expected: -0.00625 },
{ name: 'pinch strong', deltaY: 3, expected: -0.0375 }
]

cases.forEach(({ name, deltaY, expected }) => {
it(`${name} scales proportionally`, () => {
const delta = wheelZoomDelta(deltaY, DOM_DELTA_PIXEL)

expect(delta).toBeCloseTo(expected)
expect(delta).not.toBe(signBasedWheelDelta(deltaY))
})
})
})

describe('zero deltaY', () => {
it('ignores horizontal wheel and gesture end', () => {
expect(wheelZoomDelta(0, DOM_DELTA_PIXEL)).toBe(0)
expect(wheelZoomDelta(0, DOM_DELTA_PIXEL)).toBe(signBasedWheelDelta(0))
})
})

describe('small discrete wheel delta', () => {
it('uses proportional step below mouse cap threshold', () => {
const delta = wheelZoomDelta(4, DOM_DELTA_PIXEL)

expect(delta).toBeCloseTo(-0.05)
expect(delta).not.toBe(signBasedWheelDelta(4))
})
})
133 changes: 133 additions & 0 deletions test/zoom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { OnZoom, Zoom } from '../src/zoom'

const intensity = 0.1

const DOM_DELTA_PIXEL = 0
const DOM_DELTA_LINE = 1
const DOM_DELTA_PAGE = 2

class TestableZoom extends Zoom {
public setup(element: HTMLElement, onzoom: OnZoom) {
this.element = element
this.onzoom = onzoom
}

public triggerWheel(event: Partial<WheelEvent>) {
this.wheel(event as WheelEvent)
}
}

function createWheelEvent(overrides: Partial<WheelEvent> = {}): Partial<WheelEvent> {
return {
deltaY: 0,
deltaMode: 0,
clientX: 50,
clientY: 50,
preventDefault: jest.fn(),
...overrides
}
}

function createHolder(): HTMLElement {
return {
getBoundingClientRect: () => ({
left: 0,
top: 0,
right: 100,
bottom: 100,
width: 100,
height: 100,
x: 0,
y: 0,
toJSON: () => ({})
})
} as unknown as HTMLElement
}

function triggerWheel(deltaY: number, deltaMode: number) {
const onzoom = jest.fn()
const zoom = new TestableZoom(intensity)

zoom.setup(createHolder(), onzoom)
zoom.triggerWheel(createWheelEvent({ deltaY, deltaMode }))

return onzoom
}

describe('Zoom wheel handler', () => {
describe('mouse-like wheel zoom', () => {
const cases = [
{ name: 'Chrome wheel down', deltaY: 120, deltaMode: DOM_DELTA_PIXEL, expected: -intensity },
{ name: 'Chrome wheel up', deltaY: -120, deltaMode: DOM_DELTA_PIXEL, expected: intensity },
{ name: 'Firefox wheel down', deltaY: 1, deltaMode: DOM_DELTA_LINE, expected: -intensity },
{ name: 'Firefox wheel up', deltaY: -1, deltaMode: DOM_DELTA_LINE, expected: intensity },
{ name: 'page scroll down', deltaY: 1, deltaMode: DOM_DELTA_PAGE, expected: -intensity }
]

cases.forEach(({ name, deltaY, deltaMode, expected }) => {
it(name, () => {
const onzoom = triggerWheel(deltaY, deltaMode)

expect(onzoom).toHaveBeenCalledWith(
expected,
expect.any(Number),
expect.any(Number),
'wheel'
)
})
})
})

describe('touchpad wheel zoom', () => {
const cases = [
{ name: 'pinch light', deltaY: 0.05, expected: -0.000625 },
{ name: 'pinch medium', deltaY: 0.5, expected: -0.00625 },
{ name: 'pinch strong', deltaY: 3, expected: -0.0375 }
]

cases.forEach(({ name, deltaY, expected }) => {
it(name, () => {
const onzoom = triggerWheel(deltaY, DOM_DELTA_PIXEL)

expect(onzoom).toHaveBeenCalledWith(
expect.closeTo(expected),
expect.any(Number),
expect.any(Number),
'wheel'
)
})
})
})

describe('zero deltaY', () => {
it('ignores horizontal wheel and gesture end', () => {
const onzoom = triggerWheel(0, DOM_DELTA_PIXEL)

expect(onzoom).not.toHaveBeenCalled()
})
})

describe('small discrete wheel delta', () => {
it('uses proportional step below mouse cap threshold', () => {
const onzoom = triggerWheel(4, DOM_DELTA_PIXEL)

expect(onzoom).toHaveBeenCalledWith(
expect.closeTo(-0.05),
expect.any(Number),
expect.any(Number),
'wheel'
)
})
})

it('calls preventDefault on wheel', () => {
const onzoom = jest.fn()
const zoom = new TestableZoom(intensity)
const event = createWheelEvent({ deltaY: 120 })

zoom.setup(createHolder(), onzoom)
zoom.triggerWheel(event)

expect(event.preventDefault).toHaveBeenCalled()
})
})
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
"DOM"
]
},
"include": ["src"]
"include": ["src", "test"]
}
Loading