Skip to content

Commit 05aaf43

Browse files
authored
Merge pull request #29 from gtm-nayan/generic-action-type
2 parents 4c88909 + ce97375 commit 05aaf43

18 files changed

+314
-267
lines changed
File renamed without changes.

.prettierrc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"useTabs": true,
3+
"printWidth": 100,
4+
"singleQuote": true
5+
}

package-lock.json

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
{
22
"name": "svelte-actions",
33
"version": "0.2.1",
4-
"module": "dist/esm/index.mjs",
5-
"main": "dist/index.js",
4+
"type": "module",
65
"types": "dist/index.d.ts",
76
"scripts": {
8-
"build": "tsc && npm run build:esm && npm run rename:esm",
9-
"build:esm": "tsc --module es2015 --target es5 --outdir dist/esm",
10-
"rename:esm": "find ./dist/esm -name \"*.js\" -exec bash -c 'mv \"$1\" \"${1%.js}\".mjs' - '{}' \\;",
11-
"copy:esm": "mv ./dist/esm/*.mjs ./dist/",
7+
"build": "tsc",
128
"preversion": "npm run build",
139
"version": "auto-changelog -p --template keepachangelog && git add CHANGELOG.md",
1410
"prepublishOnly": "git push && git push --tags && gh-release",
15-
"test": "mocha ./src/test.ts"
11+
"test": "mocha ./src/test.ts",
12+
"format": "prettier --write src/**/*.ts",
13+
"check": "tsc --noEmit"
1614
},
1715
"keywords": [
1816
"svelte"
@@ -41,9 +39,13 @@
4139
"gh-release": "^4.0.3",
4240
"jsdom": "^16.4.0",
4341
"mocha": "^9.2.0",
42+
"prettier": "^2.5.1",
4443
"sinon": "^9.2.3",
4544
"sucrase": "^3.17.0",
4645
"svelte": "^3.29.4",
4746
"typescript": "^4.0.5"
47+
},
48+
"exports": {
49+
"import": "./dist/index.js"
4850
}
4951
}

src/clickOutside.test.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,57 @@
1-
import { clickOutside } from './clickOutside';
2-
import { Action } from './types';
31
import * as assert from 'assert';
42
import * as sinon from 'sinon';
3+
import { clickOutside } from './clickOutside';
54

6-
describe('clickOutside', function() {
5+
describe('clickOutside', function () {
76
let element: HTMLElement;
87
let sibling: HTMLElement;
9-
let action: ReturnType<Action>;
8+
let action: ReturnType<typeof clickOutside>;
109

11-
before(function() {
10+
before(function () {
1211
element = document.createElement('div');
1312
sibling = document.createElement('div');
1413
document.body.appendChild(element);
1514
document.body.appendChild(sibling);
1615
});
1716

18-
after(function() {
17+
after(function () {
1918
element.remove();
2019
sibling.remove();
2120
});
2221

23-
afterEach(function() {
22+
afterEach(function () {
2423
action.destroy!();
2524
});
2625

27-
it('calls callback on outside click', function() {
26+
it('calls callback on outside click', function () {
2827
const cb = sinon.fake();
2928
action = clickOutside(element, { enabled: true, cb });
3029

3130
sibling.click();
3231
assert.ok(cb.calledOnce);
3332
});
3433

35-
it('does not call callback when disabled', function() {
34+
it('does not call callback when disabled', function () {
3635
const cb = sinon.fake();
3736
action = clickOutside(element, { enabled: false, cb });
3837

3938
sibling.click();
4039
assert.ok(cb.notCalled);
4140
});
4241

43-
it('does not call callback when element clicked', function() {
42+
it('does not call callback when element clicked', function () {
4443
const cb = sinon.fake();
4544
action = clickOutside(element, { enabled: true, cb });
4645

4746
element.click();
4847
assert.ok(cb.notCalled);
4948
});
5049

51-
it('updates parameters', function() {
50+
it('updates parameters', function () {
5251
const cb = sinon.fake();
5352
action = clickOutside(element, { enabled: true, cb });
5453

54+
// @ts-expect-error
5555
action.update!({ enabled: false });
5656
element.click();
5757
assert.ok(cb.notCalled);

src/clickOutside.ts

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,34 @@
11
import { Action } from './types';
22

3+
export interface ClickOutsideConfig {
4+
enabled: boolean;
5+
cb: (node: HTMLElement) => void;
6+
}
37
/**
4-
*
58
* Call callback when user clicks outside a given element
6-
*
7-
* Usage:
9+
*
10+
* @example
11+
* ```svelte
812
* <div use:clickOutside={{ enabled: open, cb: () => open = false }}>
9-
*
13+
* ```
1014
* Demo: https://svelte.dev/repl/dae848c2157e48ab932106779960f5d5?version=3.19.2
11-
*
1215
*/
13-
export function clickOutside(node: HTMLElement, params: {enabled: boolean, cb: Function }): ReturnType<Action> {
14-
const { enabled: initialEnabled, cb } = params
15-
16-
const handleOutsideClick = ({ target }: MouseEvent) => {
17-
if (!node.contains(target as Node)) cb(node); // typescript hack, not sure how to solve without asserting as Node
18-
};
16+
export const clickOutside: Action<ClickOutsideConfig> = (node, config) => {
17+
function handler(e: MouseEvent) {
18+
if (!node.contains(e.target as Node)) config.cb(node);
19+
}
1920

20-
function update({enabled}: {enabled: boolean}) {
21-
if (enabled) {
22-
window.addEventListener('click', handleOutsideClick);
23-
} else {
24-
window.removeEventListener('click', handleOutsideClick);
25-
}
26-
}
27-
update({ enabled: initialEnabled });
28-
return {
29-
update,
30-
destroy() {
31-
window.removeEventListener( 'click', handleOutsideClick );
32-
}
33-
};
21+
function set_handler(enabled: boolean) {
22+
(enabled ? window.addEventListener : window.removeEventListener)('click', handler);
23+
}
24+
set_handler(config.enabled);
3425

35-
}
26+
return {
27+
update(params) {
28+
set_handler((config = params).enabled);
29+
},
30+
destroy() {
31+
set_handler(false);
32+
},
33+
};
34+
};

src/lazyload.test.ts

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,63 @@
1-
import { lazyload } from './lazyload';
2-
import { Action } from './types';
31
import * as assert from 'assert';
42
import * as sinon from 'sinon';
3+
import { lazyload } from './lazyload';
54

6-
describe('lazyload', function() {
5+
describe('lazyload', function () {
76
let element: HTMLElement;
8-
let action: ReturnType<Action>;
7+
let action: ReturnType<typeof lazyload>;
98
let intersectionObserverConstructorSpy: sinon.SinonSpy;
109
const observeFake = sinon.fake();
1110
const unobserveFake = sinon.fake();
1211

13-
before(function() {
12+
before(function () {
1413
setupIntersectionObserverMock({
1514
observe: observeFake,
16-
unobserve: unobserveFake
15+
unobserve: unobserveFake,
1716
});
1817
intersectionObserverConstructorSpy = sinon.spy(global, 'IntersectionObserver');
1918
});
2019

21-
beforeEach(function() {
20+
beforeEach(function () {
2221
element = document.createElement('div');
2322
document.body.appendChild(element);
2423
});
2524

26-
afterEach(function() {
25+
afterEach(function () {
2726
action.destroy!();
2827
element.remove();
2928
observeFake.resetHistory();
3029
unobserveFake.resetHistory();
3130
});
3231

33-
it('observes node', function() {
32+
it('observes node', function () {
3433
action = lazyload(element, {});
3534
assert.ok(intersectionObserverConstructorSpy.calledOnce);
3635
assert.ok(observeFake.calledOnce);
3736
});
3837

39-
it('sets attribute on intersection', function() {
40-
action = lazyload(element, { className: 'test'});
38+
it('sets attribute on intersection', function () {
39+
action = lazyload(element, { className: 'test' });
4140
const intersectionCallback = intersectionObserverConstructorSpy.firstCall.firstArg;
42-
intersectionCallback([{
43-
isIntersecting: true,
44-
target: element
45-
}]);
41+
intersectionCallback([
42+
{
43+
isIntersecting: true,
44+
target: element,
45+
},
46+
]);
4647

4748
assert.ok(unobserveFake.calledOnce);
4849
assert.strictEqual(element.className, 'test');
4950
});
5051

51-
it('does not set attribute when no intersection', function() {
52-
action = lazyload(element, { className: 'test'});
52+
it('does not set attribute when no intersection', function () {
53+
action = lazyload(element, { className: 'test' });
5354
const intersectionCallback = intersectionObserverConstructorSpy.firstCall.firstArg;
54-
intersectionCallback([{
55-
isIntersecting: false,
56-
target: element
57-
}]);
55+
intersectionCallback([
56+
{
57+
isIntersecting: false,
58+
target: element,
59+
},
60+
]);
5861

5962
assert.ok(unobserveFake.notCalled);
6063
assert.strictEqual(element.className, '');
@@ -69,7 +72,7 @@ function setupIntersectionObserverMock({
6972
disconnect = () => null,
7073
observe = () => null,
7174
takeRecords = () => [],
72-
unobserve = () => null
75+
unobserve = () => null,
7376
} = {}): void {
7477
class MockIntersectionObserver implements IntersectionObserver {
7578
readonly root: Element | null = root;
@@ -84,12 +87,12 @@ function setupIntersectionObserverMock({
8487
Object.defineProperty(window, 'IntersectionObserver', {
8588
writable: true,
8689
configurable: true,
87-
value: MockIntersectionObserver
90+
value: MockIntersectionObserver,
8891
});
8992

9093
Object.defineProperty(global, 'IntersectionObserver', {
9194
writable: true,
9295
configurable: true,
93-
value: MockIntersectionObserver
96+
value: MockIntersectionObserver,
9497
});
9598
}

src/lazyload.ts

Lines changed: 26 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,35 @@
11
import { Action } from './types';
2+
const node_attributes_map = new WeakMap<HTMLElement, object>();
23

4+
const intersection_handler: IntersectionObserverCallback = (entries) => {
5+
entries.forEach((entry) => {
6+
if (entry.isIntersecting && entry.target instanceof HTMLElement) {
7+
const node = entry.target;
8+
Object.assign(node, node_attributes_map.get(node));
9+
lazy_load_observer.unobserve(node);
10+
}
11+
});
12+
};
13+
14+
let lazy_load_observer: IntersectionObserver;
15+
function observer() {
16+
return (lazy_load_observer ??= new IntersectionObserver(intersection_handler));
17+
}
318
/**
4-
* Attach onto any image to lazy load it
5-
*
19+
* Set attributes on an element when it is visible in the viewport.
20+
*@example
21+
*```svelte
622
* <img use:lazyLoad={{src:"/myimage"}} alt="">
7-
*
23+
*```
824
* Demo: https://svelte.dev/repl/f12988de576b4bf9b541a2a59eb838f6?version=3.23.2
9-
*
25+
*
1026
*/
11-
const lazyLoadHandleIntersection: IntersectionObserverCallback = (entries) => {
12-
entries.forEach(
13-
entry => {
14-
if (!entry.isIntersecting) {
15-
return
16-
}
17-
18-
if (!(entry.target instanceof HTMLElement)) {
19-
return;
20-
}
21-
22-
let node = entry.target;
23-
let attributes = lazyLoadNodeAttributes.find(item => item.node === node)?.attributes
24-
Object.assign(node, attributes)
25-
26-
lazyLoadObserver.unobserve(node)
27-
}
28-
)
29-
}
30-
31-
let lazyLoadObserver: IntersectionObserver;
32-
let lazyLoadNodeAttributes: Array<{node: HTMLElement, attributes: Object}> = []
33-
export function lazyload(node: HTMLElement, attributes: Object): ReturnType<Action> {
34-
if (!lazyLoadObserver) {
35-
lazyLoadObserver = new IntersectionObserver(lazyLoadHandleIntersection);
36-
}
37-
lazyLoadNodeAttributes.push({node, attributes})
38-
39-
lazyLoadObserver.observe(node);
27+
export const lazyload: Action<object> = (node, attributes) => {
28+
node_attributes_map.set(node, attributes);
29+
observer().observe(node);
4030
return {
4131
destroy() {
42-
lazyLoadObserver.unobserve(node);
43-
}
32+
observer().unobserve(node);
33+
},
4434
};
45-
}
35+
};

0 commit comments

Comments
 (0)