Skip to content

Commit b93e36f

Browse files
author
Lukas Oppermann
authored
WIP: Extract hoverClass to module (#409)
* move hoverClass to module * typescript fix * adding new tests * add more test * add throttle tests * removing empty space * improve throttle tests * update throttle test to include predefined threshold
1 parent d3e7e3d commit b93e36f

10 files changed

+221
-70
lines changed

__tests__/events/hoverClass.test.ts

+91-36
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,116 @@
11
/* global describe,test,expect,beforeEach,CustomEvent */
2-
import sortable from '../../src/html5sortable'
2+
import enableHoverClass from '../../src/hoverClass'
3+
import store from '../../src/store'
34
import {mockInnerHTML} from '../helpers'
45

56
describe('Testing mouseenter and mouseleave events for hoverClass', () => {
6-
let ul, li
7+
let ul, li, liSecond
78
// setup html
89
beforeEach(() => {
910
document.body.innerHTML = mockInnerHTML
1011
ul = document.querySelector('.sortable')
1112
li = ul.querySelector('li')
13+
liSecond = ul.querySelector('.li-second')
14+
// setup data
15+
store(ul).config = {
16+
hoverClass: 'hover-class',
17+
items: 'li',
18+
throttleTime: 0
19+
}
20+
// enable module
21+
enableHoverClass(ul, true)
1222
})
1323

14-
test('should not add class on hover event', () => {
15-
// setup sortable with no hoverClass
16-
sortable(ul, {
17-
items: 'li',
18-
hoverClass: false
19-
})
24+
test('should not add class on hover event when disabled', () => {
25+
// disable hover class
26+
enableHoverClass(ul, false)
27+
store(ul).setConfig('hoverClass', null)
2028
// get inital class list length
2129
let classListLength = li.classList.length
2230
// trigger mouseenter event
23-
li.dispatchEvent(new CustomEvent('mouseenter'))
31+
li.dispatchEvent(new MouseEvent('mousemove', {
32+
bubbles: true,
33+
cancelable: true,
34+
buttons: 0,
35+
target: li
36+
}))
2437
// assert that class list lenght did not change
2538
expect(li.classList.length).toBe(classListLength)
2639
})
27-
28-
test('should correctly add class on hover event', () => {
29-
// setup sortable with hoverClass
30-
sortable(ul, {
31-
'items': 'li',
32-
hoverClass: 'sortable-item-over'
33-
})
40+
41+
test('should not add class on hover event hoverClass is null', () => {
42+
// disable hover class
43+
enableHoverClass(ul, false)
44+
store(ul).setConfig('hoverClass', null)
45+
enableHoverClass(ul, true)
46+
// get inital class list length
47+
let classListLength = li.classList.length
3448
// trigger mouseenter event
35-
li.dispatchEvent(new CustomEvent('mouseenter'))
49+
li.dispatchEvent(new MouseEvent('mousemove', {
50+
bubbles: true,
51+
cancelable: true,
52+
buttons: 0,
53+
target: li
54+
}))
55+
// assert that class list lenght did not change
56+
expect(li.classList.length).toBe(classListLength)
57+
})
58+
59+
test('should correctly add class on hover event, and remove on hover other element', () => {
60+
expect(li.classList.contains('hover-class')).toBe(false)
61+
// trigger mouse event
62+
li.dispatchEvent(new MouseEvent('mousemove', {
63+
bubbles: true,
64+
cancelable: true,
65+
buttons: 0,
66+
target: li
67+
}))
3668
// assert that class was added
37-
expect(li.classList.contains('sortable-item-over')).toBe(true)
38-
// trigger mouseleave event
39-
li.dispatchEvent(new CustomEvent('mouseleave'))
69+
expect(li.classList.contains('hover-class')).toBe(true)
70+
71+
// trigger mouseleave events
72+
liSecond.dispatchEvent(new MouseEvent('mousemove', {
73+
bubbles: true,
74+
cancelable: true,
75+
buttons: 0,
76+
target: liSecond
77+
}))
4078
// assert that class was removed
41-
expect(li.classList.contains('sortable-item-over')).toBe(false)
79+
expect(liSecond.classList.contains('hover-class')).toBe(true)
80+
expect(li.classList.contains('hover-class')).toBe(false)
4281
})
4382

44-
test('should correctly add and remove both classes on hover event', () => {
45-
// setup sortable with multiple hoverClasses
46-
sortable(ul, {
47-
'items': 'li',
48-
hoverClass: 'sortable-item-over sortable-item-over-second'
49-
})
50-
// trigger mouseenter event
51-
li.dispatchEvent(new CustomEvent('mouseenter'))
52-
// assert that class were added
53-
expect(li.classList.contains('sortable-item-over')).toBe(true)
54-
expect(li.classList.contains('sortable-item-over-second')).toBe(true)
55-
// trigger mouseleave event
56-
li.dispatchEvent(new CustomEvent('mouseleave'))
83+
test('should remove class when leaving sortable', () => {
84+
// trigger mouse event
85+
li.dispatchEvent(new MouseEvent('mousemove', {
86+
bubbles: true,
87+
cancelable: true,
88+
buttons: 0,
89+
target: li
90+
}))
91+
// assert that class was added
92+
expect(li.classList.contains('hover-class')).toBe(true)
93+
// trigger mouseleave events
94+
ul.dispatchEvent(new MouseEvent('mouseleave', {
95+
bubbles: true,
96+
cancelable: true,
97+
buttons: 0,
98+
target: ul
99+
}))
57100
// assert that class was removed
58-
expect(li.classList.contains('sortable-item-over')).toBe(false)
59-
expect(li.classList.contains('sortable-item-over-second')).toBe(false)
101+
expect(li.classList.contains('hover-class')).toBe(false)
102+
})
103+
104+
test('should not fire when button is pressed', () => {
105+
// trigger mouse event
106+
li.dispatchEvent(new MouseEvent('mousemove', {
107+
bubbles: true,
108+
cancelable: true,
109+
buttons: 1
110+
target: li
111+
}))
112+
// assert that class was added
113+
expect(li.classList.contains('hover-class')).toBe(false)
60114
})
115+
61116
})

__tests__/helpers.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const mockInnerHTML: string = `
66
<span class="handle"></span>
77
item
88
</li>
9-
<li>
9+
<li class="li-second">
1010
<span class="another-handle"></span>
1111
item 2
1212
</li>

__tests__/throttle.test.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/* global describe,test,expect */
2+
/* eslint-env jest */
3+
import _throttle from '../src/throttle'
4+
5+
describe('Testing throttle', () => {
6+
test('throttle should not allow functions to be called multiple times within the timeframe', () => {
7+
let value = 0
8+
let fn = _throttle(() => {
9+
value++
10+
})
11+
// call function twice immeditatly
12+
fn()
13+
fn()
14+
// assert
15+
expect(value).toBe(1)
16+
})
17+
18+
test('throttle should allow functions to be called multiple times after the timeframe', () => {
19+
let value = 0
20+
let fn = _throttle(() => {
21+
value++
22+
}, 10)
23+
// call function twice immeditatly
24+
global.Date.now = jest.fn(() => 1490760656000)
25+
fn()
26+
global.Date.now = jest.fn(() => 1490760657000)
27+
fn()
28+
// assert
29+
expect(value).toBe(2)
30+
})
31+
32+
test('throttle should fail if no functin is provided', () => {
33+
// assert
34+
expect(() => { _throttle('test', 10) }).toThrowError('You must provide a function as the first argument for throttle.')
35+
})
36+
37+
test('throttle should fail if threshold is not a number', () => {
38+
// assert
39+
expect(() => { _throttle(() => { }, '10') }).toThrowError('You must provide a number as the second argument for throttle.')
40+
})
41+
})

docs/index.html

+2-1
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,8 @@ <h2 class="h4 mt1">Copy items here</h2>
493493
sortable('.js-sortable-buttons', {
494494
forcePlaceholderSize: true,
495495
items: 'li',
496-
placeholderClass: 'border border-white mb1'
496+
placeholderClass: 'border border-white mb1',
497+
hoverClass: 'bg-yellow'
497498
});
498499
// buttons to add items and reload the list
499500
// separately to showcase issue without reload

src/defaultConfiguration.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export default {
1414
draggingClass: 'sortable-dragging',
1515
hoverClass: false,
1616
debounce: 0,
17+
throttleTime: 100,
1718
maxItems: 0,
1819
itemSerializer: undefined,
1920
containerSerializer: undefined,

src/hoverClass.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/* eslint-env browser */
2+
import store from './store'
3+
import _filter from './filter'
4+
import _throttle from './throttle'
5+
import { addEventListener as _on, removeEventListener as _off } from './eventListener'
6+
/**
7+
* enable or disable hoverClass on mouseenter/leave if container Items
8+
* @param {sortable} sortableContainer a valid sortableContainer
9+
* @param {boolean} enable enable or disable event
10+
*/
11+
// export default (sortableContainer: sortable, enable: boolean) => {
12+
export default (sortableContainer: sortable, enable: boolean) => {
13+
if (typeof store(sortableContainer).getConfig('hoverClass') === 'string') {
14+
let hoverClasses = store(sortableContainer).getConfig('hoverClass').split(' ')
15+
// add class on hover
16+
if (enable === true) {
17+
_on(sortableContainer, 'mousemove', _throttle((event) => {
18+
// check of no mouse button was pressed when mousemove started == no drag
19+
if (event.buttons === 0) {
20+
_filter(sortableContainer.children, store(sortableContainer).getConfig('items')).forEach(item => {
21+
if (item !== event.target) {
22+
item.classList.remove(...hoverClasses)
23+
} else {
24+
item.classList.add(...hoverClasses)
25+
}
26+
})
27+
}
28+
}, store(sortableContainer).getConfig('throttleTime')))
29+
// remove class on leave
30+
_on(sortableContainer, 'mouseleave', () => {
31+
_filter(sortableContainer.children, store(sortableContainer).getConfig('items')).forEach(item => {
32+
item.classList.remove(...hoverClasses)
33+
})
34+
})
35+
// remove events
36+
} else {
37+
_off(sortableContainer, 'mousemove')
38+
_off(sortableContainer, 'mouseleave')
39+
}
40+
}
41+
}

src/html5sortable.ts

+3-17
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import setDragImage from './setDragImage'
1818
import {default as store, stores} from './store'
1919
import _listsConnected from './isConnected'
2020
import defaultConfiguration from './defaultConfiguration'
21+
import enableHoverClass from './hoverClass'
2122
/*
2223
* variables global to the plugin
2324
*/
@@ -283,23 +284,8 @@ export default function sortable (sortableElements, options: object|string|undef
283284
_enableSortable(sortableElement)
284285
_attr(listItems, 'role', 'option')
285286
_attr(listItems, 'aria-grabbed', 'false')
286-
287-
// Mouse over class
288-
// TODO - only assign hoverClass if not dragging
289-
if (typeof options.hoverClass === 'string') {
290-
let hoverClasses = options.hoverClass.split(' ')
291-
// add class on hover
292-
_on(listItems, 'mouseenter', function (e) {
293-
if (!dragging) {
294-
e.target.classList.add(...hoverClasses)
295-
}
296-
})
297-
// remove class on leave
298-
_on(listItems, 'mouseleave', function (e) {
299-
e.target.classList.remove(...hoverClasses)
300-
})
301-
}
302-
287+
// enable hover class
288+
enableHoverClass(sortableElement, listItems, true)
303289
/*
304290
Handle drag events on draggable items
305291
Handle is set at the sortableElement level as it will bubble up

src/throttle.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* make sure a function is only called once within the given amount of time
3+
* @param {Function} fn the function to throttle
4+
* @param {number} threshold time limit for throttling
5+
*/
6+
// must use function to keep this context
7+
export default function (fn: Function, threshold: number = 250) {
8+
// check function
9+
if (typeof fn !== 'function') {
10+
throw new Error('You must provide a function as the first argument for throttle.')
11+
}
12+
// check threshold
13+
if (typeof threshold !== 'number') {
14+
throw new Error('You must provide a number as the second argument for throttle.')
15+
}
16+
17+
let lastEventTimestamp = null
18+
19+
return (...args) => {
20+
let now = Date.now()
21+
if (lastEventTimestamp === null || now - lastEventTimestamp >= threshold) {
22+
lastEventTimestamp = now
23+
fn.apply(this, args)
24+
}
25+
}
26+
}

src/types/configuration.d.ts

+14-13
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
interface configuration {
2-
connectWith: boolean,
3-
acceptFrom: void,
4-
copy: boolean,
5-
placeholder: void,
6-
disableIEFix: boolean,
7-
placeholderClass: string,
8-
draggingClass: string,
9-
hoverClass: boolean,
10-
debounce: number,
11-
maxItems: number,
12-
itemSerializer: void,
13-
containerSerializer: void,
14-
items: string
2+
connectWith?: string,
3+
acceptFrom?: void,
4+
copy?: boolean,
5+
placeholder?: void,
6+
disableIEFix?: boolean,
7+
placeholderClass?: string,
8+
draggingClass?: string,
9+
hoverClass?: string,
10+
debounce?: number,
11+
throttleTime?: number,
12+
maxItems?: number,
13+
itemSerializer?: void,
14+
containerSerializer?: void,
15+
items?: string
1516
}

tsconfig.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
{
22
"compilerOptions": {
33
"module": "commonjs",
4-
"target": "ESNext",
5-
"strictNullChecks": true
4+
"target": "ESNext"
65
},
76
"typeRoots": [
87
"./src/types",

0 commit comments

Comments
 (0)