Skip to content

Commit

Permalink
Merge pull request #29 from gtm-nayan/generic-action-type
Browse files Browse the repository at this point in the history
  • Loading branch information
swyxio committed Mar 7, 2022
2 parents 4c88909 + ce97375 commit 05aaf43
Show file tree
Hide file tree
Showing 18 changed files with 314 additions and 267 deletions.
File renamed without changes.
5 changes: 5 additions & 0 deletions .prettierrc
@@ -0,0 +1,5 @@
{
"useTabs": true,
"printWidth": 100,
"singleQuote": true
}
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 9 additions & 7 deletions package.json
@@ -1,18 +1,16 @@
{
"name": "svelte-actions",
"version": "0.2.1",
"module": "dist/esm/index.mjs",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc && npm run build:esm && npm run rename:esm",
"build:esm": "tsc --module es2015 --target es5 --outdir dist/esm",
"rename:esm": "find ./dist/esm -name \"*.js\" -exec bash -c 'mv \"$1\" \"${1%.js}\".mjs' - '{}' \\;",
"copy:esm": "mv ./dist/esm/*.mjs ./dist/",
"build": "tsc",
"preversion": "npm run build",
"version": "auto-changelog -p --template keepachangelog && git add CHANGELOG.md",
"prepublishOnly": "git push && git push --tags && gh-release",
"test": "mocha ./src/test.ts"
"test": "mocha ./src/test.ts",
"format": "prettier --write src/**/*.ts",
"check": "tsc --noEmit"
},
"keywords": [
"svelte"
Expand Down Expand Up @@ -41,9 +39,13 @@
"gh-release": "^4.0.3",
"jsdom": "^16.4.0",
"mocha": "^9.2.0",
"prettier": "^2.5.1",
"sinon": "^9.2.3",
"sucrase": "^3.17.0",
"svelte": "^3.29.4",
"typescript": "^4.0.5"
},
"exports": {
"import": "./dist/index.js"
}
}
22 changes: 11 additions & 11 deletions src/clickOutside.test.ts
@@ -1,57 +1,57 @@
import { clickOutside } from './clickOutside';
import { Action } from './types';
import * as assert from 'assert';
import * as sinon from 'sinon';
import { clickOutside } from './clickOutside';

describe('clickOutside', function() {
describe('clickOutside', function () {
let element: HTMLElement;
let sibling: HTMLElement;
let action: ReturnType<Action>;
let action: ReturnType<typeof clickOutside>;

before(function() {
before(function () {
element = document.createElement('div');
sibling = document.createElement('div');
document.body.appendChild(element);
document.body.appendChild(sibling);
});

after(function() {
after(function () {
element.remove();
sibling.remove();
});

afterEach(function() {
afterEach(function () {
action.destroy!();
});

it('calls callback on outside click', function() {
it('calls callback on outside click', function () {
const cb = sinon.fake();
action = clickOutside(element, { enabled: true, cb });

sibling.click();
assert.ok(cb.calledOnce);
});

it('does not call callback when disabled', function() {
it('does not call callback when disabled', function () {
const cb = sinon.fake();
action = clickOutside(element, { enabled: false, cb });

sibling.click();
assert.ok(cb.notCalled);
});

it('does not call callback when element clicked', function() {
it('does not call callback when element clicked', function () {
const cb = sinon.fake();
action = clickOutside(element, { enabled: true, cb });

element.click();
assert.ok(cb.notCalled);
});

it('updates parameters', function() {
it('updates parameters', function () {
const cb = sinon.fake();
action = clickOutside(element, { enabled: true, cb });

// @ts-expect-error
action.update!({ enabled: false });
element.click();
assert.ok(cb.notCalled);
Expand Down
51 changes: 25 additions & 26 deletions src/clickOutside.ts
@@ -1,35 +1,34 @@
import { Action } from './types';

export interface ClickOutsideConfig {
enabled: boolean;
cb: (node: HTMLElement) => void;
}
/**
*
* Call callback when user clicks outside a given element
*
* Usage:
*
* @example
* ```svelte
* <div use:clickOutside={{ enabled: open, cb: () => open = false }}>
*
* ```
* Demo: https://svelte.dev/repl/dae848c2157e48ab932106779960f5d5?version=3.19.2
*
*/
export function clickOutside(node: HTMLElement, params: {enabled: boolean, cb: Function }): ReturnType<Action> {
const { enabled: initialEnabled, cb } = params

const handleOutsideClick = ({ target }: MouseEvent) => {
if (!node.contains(target as Node)) cb(node); // typescript hack, not sure how to solve without asserting as Node
};
export const clickOutside: Action<ClickOutsideConfig> = (node, config) => {
function handler(e: MouseEvent) {
if (!node.contains(e.target as Node)) config.cb(node);
}

function update({enabled}: {enabled: boolean}) {
if (enabled) {
window.addEventListener('click', handleOutsideClick);
} else {
window.removeEventListener('click', handleOutsideClick);
}
}
update({ enabled: initialEnabled });
return {
update,
destroy() {
window.removeEventListener( 'click', handleOutsideClick );
}
};
function set_handler(enabled: boolean) {
(enabled ? window.addEventListener : window.removeEventListener)('click', handler);
}
set_handler(config.enabled);

}
return {
update(params) {
set_handler((config = params).enabled);
},
destroy() {
set_handler(false);
},
};
};
51 changes: 27 additions & 24 deletions src/lazyload.test.ts
@@ -1,60 +1,63 @@
import { lazyload } from './lazyload';
import { Action } from './types';
import * as assert from 'assert';
import * as sinon from 'sinon';
import { lazyload } from './lazyload';

describe('lazyload', function() {
describe('lazyload', function () {
let element: HTMLElement;
let action: ReturnType<Action>;
let action: ReturnType<typeof lazyload>;
let intersectionObserverConstructorSpy: sinon.SinonSpy;
const observeFake = sinon.fake();
const unobserveFake = sinon.fake();

before(function() {
before(function () {
setupIntersectionObserverMock({
observe: observeFake,
unobserve: unobserveFake
unobserve: unobserveFake,
});
intersectionObserverConstructorSpy = sinon.spy(global, 'IntersectionObserver');
});

beforeEach(function() {
beforeEach(function () {
element = document.createElement('div');
document.body.appendChild(element);
});

afterEach(function() {
afterEach(function () {
action.destroy!();
element.remove();
observeFake.resetHistory();
unobserveFake.resetHistory();
});

it('observes node', function() {
it('observes node', function () {
action = lazyload(element, {});
assert.ok(intersectionObserverConstructorSpy.calledOnce);
assert.ok(observeFake.calledOnce);
});

it('sets attribute on intersection', function() {
action = lazyload(element, { className: 'test'});
it('sets attribute on intersection', function () {
action = lazyload(element, { className: 'test' });
const intersectionCallback = intersectionObserverConstructorSpy.firstCall.firstArg;
intersectionCallback([{
isIntersecting: true,
target: element
}]);
intersectionCallback([
{
isIntersecting: true,
target: element,
},
]);

assert.ok(unobserveFake.calledOnce);
assert.strictEqual(element.className, 'test');
});

it('does not set attribute when no intersection', function() {
action = lazyload(element, { className: 'test'});
it('does not set attribute when no intersection', function () {
action = lazyload(element, { className: 'test' });
const intersectionCallback = intersectionObserverConstructorSpy.firstCall.firstArg;
intersectionCallback([{
isIntersecting: false,
target: element
}]);
intersectionCallback([
{
isIntersecting: false,
target: element,
},
]);

assert.ok(unobserveFake.notCalled);
assert.strictEqual(element.className, '');
Expand All @@ -69,7 +72,7 @@ function setupIntersectionObserverMock({
disconnect = () => null,
observe = () => null,
takeRecords = () => [],
unobserve = () => null
unobserve = () => null,
} = {}): void {
class MockIntersectionObserver implements IntersectionObserver {
readonly root: Element | null = root;
Expand All @@ -84,12 +87,12 @@ function setupIntersectionObserverMock({
Object.defineProperty(window, 'IntersectionObserver', {
writable: true,
configurable: true,
value: MockIntersectionObserver
value: MockIntersectionObserver,
});

Object.defineProperty(global, 'IntersectionObserver', {
writable: true,
configurable: true,
value: MockIntersectionObserver
value: MockIntersectionObserver,
});
}
62 changes: 26 additions & 36 deletions src/lazyload.ts
@@ -1,45 +1,35 @@
import { Action } from './types';
const node_attributes_map = new WeakMap<HTMLElement, object>();

const intersection_handler: IntersectionObserverCallback = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && entry.target instanceof HTMLElement) {
const node = entry.target;
Object.assign(node, node_attributes_map.get(node));
lazy_load_observer.unobserve(node);
}
});
};

let lazy_load_observer: IntersectionObserver;
function observer() {
return (lazy_load_observer ??= new IntersectionObserver(intersection_handler));
}
/**
* Attach onto any image to lazy load it
*
* Set attributes on an element when it is visible in the viewport.
*@example
*```svelte
* <img use:lazyLoad={{src:"/myimage"}} alt="">
*
*```
* Demo: https://svelte.dev/repl/f12988de576b4bf9b541a2a59eb838f6?version=3.23.2
*
*
*/
const lazyLoadHandleIntersection: IntersectionObserverCallback = (entries) => {
entries.forEach(
entry => {
if (!entry.isIntersecting) {
return
}

if (!(entry.target instanceof HTMLElement)) {
return;
}

let node = entry.target;
let attributes = lazyLoadNodeAttributes.find(item => item.node === node)?.attributes
Object.assign(node, attributes)

lazyLoadObserver.unobserve(node)
}
)
}

let lazyLoadObserver: IntersectionObserver;
let lazyLoadNodeAttributes: Array<{node: HTMLElement, attributes: Object}> = []
export function lazyload(node: HTMLElement, attributes: Object): ReturnType<Action> {
if (!lazyLoadObserver) {
lazyLoadObserver = new IntersectionObserver(lazyLoadHandleIntersection);
}
lazyLoadNodeAttributes.push({node, attributes})

lazyLoadObserver.observe(node);
export const lazyload: Action<object> = (node, attributes) => {
node_attributes_map.set(node, attributes);
observer().observe(node);
return {
destroy() {
lazyLoadObserver.unobserve(node);
}
observer().unobserve(node);
},
};
}
};

0 comments on commit 05aaf43

Please sign in to comment.