From 608c5ff3974b361ec96aefa5556d67061c824ac5 Mon Sep 17 00:00:00 2001 From: tatomyr Date: Fri, 20 Mar 2020 22:36:24 +0200 Subject: [PATCH 1/4] v10: Use purity v5; implement isActive filter service --- .debts | 2 +- cypress/integration/examples/todo.spec.js | 17 +++++++ src/config/version | 2 +- src/hashrouter.js | 2 + src/index.html | 4 +- src/index.js | 5 +- src/manifest.json | 2 +- src/modules.js | 20 ++++---- src/service-worker.js | 4 +- src/services/filters.js | 20 ++++++-- src/store/provider.js | 4 ++ src/{ => styles}/reset.css | 0 src/{ => styles}/style.css | 0 src/ui/App.js | 6 +-- src/ui/components/InputForm.js | 56 +++++++++++------------ src/ui/components/NavAboutButton.js | 24 +++++----- src/ui/components/NavItem.js | 43 ++++++++--------- src/ui/components/TasksList.js | 7 +-- 18 files changed, 117 insertions(+), 101 deletions(-) rename src/{ => styles}/reset.css (100%) rename src/{ => styles}/style.css (100%) diff --git a/.debts b/.debts index 4099407..aabe6ec 100644 --- a/.debts +++ b/.debts @@ -1 +1 @@ -23 +21 diff --git a/cypress/integration/examples/todo.spec.js b/cypress/integration/examples/todo.spec.js index fa7ab7a..2c401ec 100644 --- a/cypress/integration/examples/todo.spec.js +++ b/cypress/integration/examples/todo.spec.js @@ -139,3 +139,20 @@ context('Basic flow', () => { .should('not.have.focus') }) }) + +context.skip('Security', () => { + beforeEach(() => { + cy.visit('/#/active') + }) + // TODO: make test fail first + it('sanitizes quotes', async () => { + cy.get('#newTask').type('test" onclick="event.target.value = \'OOPS\'"') + // FIXME: why click doesn't work?? + // TODO: [!MAJOR] investigate why can't add multiple event handlers + cy.get('#newTask').click() + cy.get('#newTask').should( + 'have.value', + 'test" onclick="event.target.value = \'OOPS\'"' + ) + }) +}) diff --git a/src/config/version b/src/config/version index 37ad5c8..a13e7b9 100644 --- a/src/config/version +++ b/src/config/version @@ -1 +1 @@ -9.0.1 +10.0.0 diff --git a/src/hashrouter.js b/src/hashrouter.js index 237b82c..361fedb 100644 --- a/src/hashrouter.js +++ b/src/hashrouter.js @@ -3,6 +3,8 @@ let match // FIXME: I believe the common `match` will cause data inferring issues. Work this out. export const router = component => props => component({ ...match, ...props }) +export const getParams = () => match + export const Switch = props => { for (const [path, component] of Object.entries(props)) { const params = (path.match(/:(\w+)/g) || []).map(param => param.slice(1)) diff --git a/src/index.html b/src/index.html index 80cc921..7189800 100644 --- a/src/index.html +++ b/src/index.html @@ -6,8 +6,8 @@ Purity ToDo - - + + diff --git a/src/index.js b/src/index.js index e41bc89..3448b0d 100644 --- a/src/index.js +++ b/src/index.js @@ -7,8 +7,9 @@ mount(App) // Registering service worker // TODO: provide a maintainable DEV/PROD flagging const stage = location.hostname === 'localhost' ? 'DEV' : 'PROD' -console.log(stage, stage !== 'DEV', 'serviceWorker' in navigator) -if (stage !== 'DEV' && 'serviceWorker' in navigator) { +console.log('STAGE:', stage) +console.log('SERVICE WORKER:', 'serviceWorker' in navigator) +if (stage === 'PROD' && 'serviceWorker' in navigator) { navigator.serviceWorker.register('./service-worker.js').then(() => { console.log('Service Worker Registered') }) diff --git a/src/manifest.json b/src/manifest.json index abe0549..d31476d 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -28,7 +28,7 @@ "type": "image/png" } ], - "start_url": "/index.html", + "start_url": "/#/active", "display": "standalone", "background_color": "#5554AB", "theme_color": "#37366E" diff --git a/src/modules.js b/src/modules.js index 8ab016e..65ed964 100644 --- a/src/modules.js +++ b/src/modules.js @@ -1,17 +1,13 @@ -export { - createStore, - render, -} from 'https://tatomyr.github.io/purity/core.min.js' -export { registerAsync } from 'https://tatomyr.github.io/purity/utils/register-async.min.js' -export { debounce } from 'https://tatomyr.github.io/purity/lib/debounce.min.js' -// TODO: use minified version after fixing md5.min.js -export { md5 } from 'https://tatomyr.github.io/purity/lib/md5.js' -export { sanitize } from 'https://tatomyr.github.io/purity/lib/sanitize.min.js' +export { createStore, render } from 'https://tatomyr.github.io/purity/core.js' +export { registerAsync } from 'https://tatomyr.github.io/purity/utils/register-async.js' +export { debounce } from 'https://tatomyr.github.io/purity/utils/debounce.js' +export { md5 } from 'https://tatomyr.github.io/purity/utils/md5.js' +export { sanitize } from 'https://tatomyr.github.io/purity/utils/sanitize.js' // Local: // export { createStore, render } from 'http://192.168.1.3:8081/core.js' // export { registerAsync } from 'http://192.168.1.3:8081/utils/register-async.js' -// export { debounce } from 'http://192.168.1.3:8081/lib/debounce.js' -// export { md5 } from 'http://192.168.1.3:8081/lib/md5.js' -// export { sanitize } from 'http://192.168.1.3:8081/lib/sanitize.js' +// export { debounce } from 'http://192.168.1.3:8081/utils/debounce.js' +// export { md5 } from 'http://192.168.1.3:8081/utils/md5.js' +// export { sanitize } from 'http://192.168.1.3:8081/utils/sanitize.js' diff --git a/src/service-worker.js b/src/service-worker.js index b4d3d05..fad674a 100644 --- a/src/service-worker.js +++ b/src/service-worker.js @@ -7,8 +7,8 @@ let filesToCache = [ '/index.html', '/index.js', '/manifest.json', - '/reset.css', - '/style.css', + '/styles/reset.css', + '/styles/style.css', '/assets/images/loading-shape.gif', // IMAGES.LOADING '/assets/images/icon-pack/idea.svg', // IMAGES.UNDEFINED_TASK '/assets/images/icon-pack/forbidden.svg', // IMAGES.BROKEN diff --git a/src/services/filters.js b/src/services/filters.js index cddbe12..9ead830 100644 --- a/src/services/filters.js +++ b/src/services/filters.js @@ -1,9 +1,19 @@ export const filters = [ { id: 'all', title: 'All', filterByStatus: () => true }, - { id: 'active', title: 'Active', filterByStatus: ({ completed }) => !completed }, - { id: 'completed', title: 'Completed', filterByStatus: ({ completed }) => completed }, + { + id: 'active', + title: 'Active', + filterByStatus: ({ completed }) => !completed, + }, + { + id: 'completed', + title: 'Completed', + filterByStatus: ({ completed }) => completed, + }, ] -export const filterByInput = input => ({ description }) => description - .toLowerCase() - .includes(input.toLowerCase()) +export const filterByInput = input => ({ description }) => + description.toLowerCase().includes(input.toLowerCase()) + +export const isActive = (input, view) => ({ id }) => + input ? id === 'all' : id === view diff --git a/src/store/provider.js b/src/store/provider.js index 718f93c..d22a5c3 100644 --- a/src/store/provider.js +++ b/src/store/provider.js @@ -1,10 +1,14 @@ import { createStore } from '/modules.js' import { stateHandler } from './state-handler.js' import asyncWatcher from './async-handler.js' +import { registerRouter, getParams } from '../hashrouter.js' export const { mount, dispatch, connect, rerender, getState } = createStore( stateHandler, asyncWatcher ) +registerRouter(rerender) + window.getState = getState +window.getParams = getParams diff --git a/src/reset.css b/src/styles/reset.css similarity index 100% rename from src/reset.css rename to src/styles/reset.css diff --git a/src/style.css b/src/styles/style.css similarity index 100% rename from src/style.css rename to src/styles/style.css diff --git a/src/ui/App.js b/src/ui/App.js index 1a0347a..e0ac9a8 100644 --- a/src/ui/App.js +++ b/src/ui/App.js @@ -1,9 +1,8 @@ import { render } from '/modules.js' -import { rerender } from '/store/provider.js' import { StartPage, TodoPage, InfoPage } from './pages/index.js' import { Notification, NavBar, TaskDetails } from './components/index.js' -import { Switch, registerRouter } from '../hashrouter.js' +import { Switch } from '../hashrouter.js' const fontStyles = font => { switch (font) { @@ -24,9 +23,6 @@ const fontStyles = font => { } } -// FIXME: do routing nicely -registerRouter(rerender) - export const App = () => { document.getElementById('applied-styles').innerHTML = fontStyles( localStorage.customFont diff --git a/src/ui/components/InputForm.js b/src/ui/components/InputForm.js index a2881af..9791b14 100644 --- a/src/ui/components/InputForm.js +++ b/src/ui/components/InputForm.js @@ -1,5 +1,5 @@ import { render, debounce, sanitize } from '/modules.js' -import { connect, dispatch } from '/store/provider.js' +import { getState, dispatch } from '/store/provider.js' import { types } from '/store/action-types.js' const onSubmit = e => { @@ -17,37 +17,35 @@ const onSubmit = e => { } const onKeyUp = debounce(e => { - dispatch({ type: types.CHANGE_INPUT, input: e.target.value }) + dispatch({ type: types.CHANGE_INPUT, input: sanitize(e.target.value) }) }, 500) const cleanInput = e => { dispatch({ type: types.CHANGE_INPUT, input: '' }) } -export const InputForm = connect( - ({ input }) => render` -
-
- - -
-
- ` -) +export const InputForm = () => render` +
+
+ + +
+
+` diff --git a/src/ui/components/NavAboutButton.js b/src/ui/components/NavAboutButton.js index 59003c0..7f98eb2 100644 --- a/src/ui/components/NavAboutButton.js +++ b/src/ui/components/NavAboutButton.js @@ -1,17 +1,15 @@ import { render } from '/modules.js' import { Icon } from './Icon.js' -import { router } from '../../hashrouter.js' +import { getParams } from '../../hashrouter.js' -export const NavAboutButton = router( - ({ view }) => render` - - ` -) +export const NavAboutButton = () => render` + +` diff --git a/src/ui/components/NavItem.js b/src/ui/components/NavItem.js index 2e98c02..3239a1c 100644 --- a/src/ui/components/NavItem.js +++ b/src/ui/components/NavItem.js @@ -1,27 +1,24 @@ import { render } from '/modules.js' -import { connect } from '/store/provider.js' -import { filterByInput } from '/services/index.js' +import { getState } from '/store/provider.js' +import { filterByInput, isActive } from '/services/index.js' import { Bubble } from './Bubble.js' -import { router } from '../../hashrouter.js' +import { getParams } from '../../hashrouter.js' -export const NavItem = router( - connect( - ({ id, title, filterByStatus, view, tasks, input }) => render` -
  • - - ${title} - - ${Bubble({ - count: tasks.filter(filterByStatus).filter(filterByInput(input)) - .length, - })} -
  • - ` - ) -) +export const NavItem = ({ id, title, filterByStatus }) => { + const { tasks, input } = getState() + const { view } = getParams() + return render` +
  • + + ${title} + + ${Bubble({ + count: tasks.filter(filterByStatus).filter(filterByInput(input)).length, + })} +
  • + ` +} diff --git a/src/ui/components/TasksList.js b/src/ui/components/TasksList.js index c19f585..531031b 100644 --- a/src/ui/components/TasksList.js +++ b/src/ui/components/TasksList.js @@ -1,6 +1,6 @@ import { render } from '/modules.js' import { connect } from '/store/provider.js' -import { filters, filterByInput } from '/services/index.js' +import { filters, filterByInput, isActive } from '/services/index.js' import { TaskItem } from './TaskItem.js' import { router } from '../../hashrouter.js' @@ -8,10 +8,7 @@ import { router } from '../../hashrouter.js' export const TasksList = router( connect(({ tasks, view, input }) => { const currentTasks = tasks - .filter( - filters.find(({ id }) => (input ? id === 'all' : id === view)) - .filterByStatus - ) + .filter(filters.find(isActive(input, view)).filterByStatus) .filter(filterByInput(input)) return render`
      From 8656371748ee9d6d0be6d1cfbc9d87c14bc8a2b0 Mon Sep 17 00:00:00 2001 From: tatomyr Date: Tue, 13 Jul 2021 22:21:55 +0300 Subject: [PATCH 2/4] v10: Change formatting; minor updates --- .prettierrc.yaml | 3 +- README.md | 2 + cypress/integration/examples/todo.spec.js | 52 ++++++----------------- src/hashrouter.js | 10 ++--- src/store/async-handler.js | 35 +++++++-------- src/store/state-handler.js | 3 ++ 6 files changed, 41 insertions(+), 64 deletions(-) diff --git a/.prettierrc.yaml b/.prettierrc.yaml index 6b9c164..0cce527 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -1,4 +1,5 @@ -trailingComma: 'es5' +trailingComma: es5 tabWidth: 2 semi: false singleQuote: true +arrowParens: avoid diff --git a/README.md b/README.md index ddbc4df..b04872f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Purity ToDo Application +[![Netlify Status](https://api.netlify.com/api/v1/badges/05f29ca9-75bf-4c65-be73-e648421a0ac6/deploy-status)](https://app.netlify.com/sites/reactive-todo-app/deploys) + The app is built using [Purity](https://github.com/tatomyr/purity) reactive store & rendering library and bare native ES modules. The aim is mostly proof of concept. diff --git a/cypress/integration/examples/todo.spec.js b/cypress/integration/examples/todo.spec.js index 2c401ec..4e306b5 100644 --- a/cypress/integration/examples/todo.spec.js +++ b/cypress/integration/examples/todo.spec.js @@ -15,25 +15,15 @@ context('Basic flow', () => { // a: cy.get('#newTask').type('One') cy.get('form#newTask-form').submit() - cy.get('ol#tasks-list li') - .should('have.length', 1) - .contains('One') + cy.get('ol#tasks-list li').should('have.length', 1).contains('One') cy.wait(0) - cy.get('#newTask') - .should('have.value', '') - .type('Two') + cy.get('#newTask').should('have.value', '').type('Two') cy.get('form#newTask-form').submit() - cy.get('ol#tasks-list li') - .should('have.length', 2) - .contains('Two') + cy.get('ol#tasks-list li').should('have.length', 2).contains('Two') cy.wait(0) - cy.get('#newTask') - .should('have.value', '') - .type('Three') + cy.get('#newTask').should('have.value', '').type('Three') cy.get('form#newTask-form').submit() - cy.get('ol#tasks-list li') - .should('have.length', 3) - .contains('Three') + cy.get('ol#tasks-list li').should('have.length', 3).contains('Three') cy.wait(0) cy.get('#all .counter').should('have.text', '3') @@ -41,17 +31,13 @@ context('Basic flow', () => { cy.get('#completed .counter').should('have.text', '0') // b: - cy.get('ol#tasks-list li:nth-child(2)') - .contains('Two') - .click() + cy.get('ol#tasks-list li:nth-child(2)').contains('Two').click() cy.get('.task-details__controls a:first-child').click() cy.get('ol#tasks-list li').should('have.length', 2) cy.get('#nav-button-completed').click() cy.get('#completed').should('have.class', 'active') - cy.get('ol#tasks-list li') - .should('have.length', 1) - .contains('Two') + cy.get('ol#tasks-list li').should('have.length', 1).contains('Two') cy.wait(0) cy.get('#all .counter').should('have.text', '3') @@ -75,9 +61,7 @@ context('Basic flow', () => { cy.get('#clear').click() cy.wait(0) - cy.get('#newTask') - .should('have.value', '') - .should('not.have.focus') + cy.get('#newTask').should('have.value', '').should('not.have.focus') cy.get('#completed').should('have.class', 'active') cy.get('#all .counter').should('have.text', '3') cy.get('#active .counter').should('have.text', '2') @@ -96,12 +80,8 @@ context('Basic flow', () => { cy.get('#all .counter').should('have.text', '4') cy.get('#active .counter').should('have.text', '3') cy.get('#completed .counter').should('have.text', '1') - cy.get('#newTask') - .should('have.value', '') - .should('not.have.focus') - cy.get('ol#tasks-list li') - .should('have.length', 3) - .contains('Four') + cy.get('#newTask').should('have.value', '').should('not.have.focus') + cy.get('ol#tasks-list li').should('have.length', 3).contains('Four') // e: cy.get('#nav-button-completed').click() @@ -119,9 +99,7 @@ context('Basic flow', () => { cy.get('#active .counter').should('have.text', '2') cy.get('#completed .counter').should('have.text', '1') - cy.get('ol#tasks-list li:last-child') - .contains('One') - .click() + cy.get('ol#tasks-list li:last-child').contains('One').click() cy.get('.task-details__controls a:first-child').click() cy.wait(0) cy.get('#active').should('have.class', 'active') @@ -134,13 +112,11 @@ context('Basic flow', () => { cy.get('#all .counter').should('have.text', '4') cy.get('#active .counter').should('have.text', '2') cy.get('#completed .counter').should('have.text', '2') - cy.get('#newTask') - .should('have.text', '') - .should('not.have.focus') + cy.get('#newTask').should('have.text', '').should('not.have.focus') }) }) -context.skip('Security', () => { +context('Security', () => { beforeEach(() => { cy.visit('/#/active') }) @@ -148,7 +124,7 @@ context.skip('Security', () => { it('sanitizes quotes', async () => { cy.get('#newTask').type('test" onclick="event.target.value = \'OOPS\'"') // FIXME: why click doesn't work?? - // TODO: [!MAJOR] investigate why can't add multiple event handlers + // TODO: [!MAJOR] investigate why can't add multiple event handlers ! cy.get('#newTask').click() cy.get('#newTask').should( 'have.value', diff --git a/src/hashrouter.js b/src/hashrouter.js index 361fedb..795276d 100644 --- a/src/hashrouter.js +++ b/src/hashrouter.js @@ -5,18 +5,18 @@ export const router = component => props => component({ ...match, ...props }) export const getParams = () => match -export const Switch = props => { - for (const [path, component] of Object.entries(props)) { +export const Switch = routes => { + for (const path in routes) { const params = (path.match(/:(\w+)/g) || []).map(param => param.slice(1)) const matchRe = new RegExp(path.replace(/:\w+/g, '(\\w+[\\w\\-\\.]*)')) const matches = window.location.hash.match(matchRe) + console.log(matches, path, ':', routes[path]) if (!matches) { continue } - const [_, ...args] = window.location.hash.match(matchRe) + const [_, ...args] = matches match = params.reduce(($, param, i) => ({ ...$, [param]: args[i] }), {}) - return (props => component({ ...props, ...match }))() - // return router(component)(props) + return routes[path](match) } } diff --git a/src/store/async-handler.js b/src/store/async-handler.js index 5904e58..3a3244d 100644 --- a/src/store/async-handler.js +++ b/src/store/async-handler.js @@ -296,23 +296,18 @@ async function startup(action, dispatch, state) { dispatch({ type: types.SET_DEFAULTS, version }) } -export default registerAsync( - { - [types.INIT]: startup, - [types.CREATE_TASK]: createTask, - [types.RESET_INPUT]: resetInput, - [types.TRIGGER_TASK]: triggerTask, - [types.DELETE_TASK]: deleteTask, - [types.UPDATE_TASK]: saveTasks, - [types.NOTIFY]: notify, - [types.MOVE_TASK]: moveTask, - [types.CHANGE_IMAGE]: changeImage, - [types.CAPTURE_PHOTO]: capturePhoto, - [types.DOWNLOAD_USER_DATA]: downloadUserData, - [types.UPLOAD_USER_DATA]: uploadUserData, - SWIPE_IMAGE: swipeImage, - }, - ({ type, ...rest }, state) => { - console.info('•', type, rest) - } -) +export default registerAsync({ + [types.INIT]: startup, + [types.CREATE_TASK]: createTask, + [types.RESET_INPUT]: resetInput, + [types.TRIGGER_TASK]: triggerTask, + [types.DELETE_TASK]: deleteTask, + [types.UPDATE_TASK]: saveTasks, + [types.NOTIFY]: notify, + [types.MOVE_TASK]: moveTask, + [types.CHANGE_IMAGE]: changeImage, + [types.CAPTURE_PHOTO]: capturePhoto, + [types.DOWNLOAD_USER_DATA]: downloadUserData, + [types.UPLOAD_USER_DATA]: uploadUserData, + SWIPE_IMAGE: swipeImage, +}) diff --git a/src/store/state-handler.js b/src/store/state-handler.js index 1fdc8a6..e02b176 100644 --- a/src/store/state-handler.js +++ b/src/store/state-handler.js @@ -19,6 +19,9 @@ const defaults = { // Main syncronous Application handler. Handle all App state changes. export const stateHandler = (state = defaults, action = {}) => { + setTimeout(() => { + console.info('•', action.type, action, state) + }) switch (action.type) { case types.ADD_TASK: return { From 75c4022934f315d0ed39d8c114761380574bbd87 Mon Sep 17 00:00:00 2001 From: Andrew Tatomyr Date: Fri, 13 May 2022 20:13:50 +0200 Subject: [PATCH 3/4] v10: use local modules --- src/modules.js | 10 +- src/purity-modules/README.md | 105 +++++++++++ src/purity-modules/core.js | 145 +++++++++++++++ src/purity-modules/debounce.js | 24 +++ src/purity-modules/debounce.min.js | 1 + src/purity-modules/delay.js | 1 + src/purity-modules/delay.min.js | 1 + src/purity-modules/md5.js | 194 ++++++++++++++++++++ src/purity-modules/pipe.js | 1 + src/purity-modules/register-async.js | 6 + src/purity-modules/register-async.min.js | 1 + src/purity-modules/register-async.test.js | 27 +++ src/purity-modules/sanitize.js | 5 + src/purity-modules/sanitize.min.js | 1 + src/purity-modules/visibility-sensor.js | 48 +++++ src/purity-modules/visibility-sensor.min.js | 1 + src/service-worker.js | 10 +- src/styles/style.css | 1 + 18 files changed, 572 insertions(+), 10 deletions(-) create mode 100644 src/purity-modules/README.md create mode 100644 src/purity-modules/core.js create mode 100644 src/purity-modules/debounce.js create mode 100644 src/purity-modules/debounce.min.js create mode 100644 src/purity-modules/delay.js create mode 100644 src/purity-modules/delay.min.js create mode 100644 src/purity-modules/md5.js create mode 100644 src/purity-modules/pipe.js create mode 100644 src/purity-modules/register-async.js create mode 100644 src/purity-modules/register-async.min.js create mode 100644 src/purity-modules/register-async.test.js create mode 100644 src/purity-modules/sanitize.js create mode 100644 src/purity-modules/sanitize.min.js create mode 100644 src/purity-modules/visibility-sensor.js create mode 100644 src/purity-modules/visibility-sensor.min.js diff --git a/src/modules.js b/src/modules.js index 65ed964..727f595 100644 --- a/src/modules.js +++ b/src/modules.js @@ -1,8 +1,8 @@ -export { createStore, render } from 'https://tatomyr.github.io/purity/core.js' -export { registerAsync } from 'https://tatomyr.github.io/purity/utils/register-async.js' -export { debounce } from 'https://tatomyr.github.io/purity/utils/debounce.js' -export { md5 } from 'https://tatomyr.github.io/purity/utils/md5.js' -export { sanitize } from 'https://tatomyr.github.io/purity/utils/sanitize.js' +export { createStore, render } from './purity-modules/core.js' +export { registerAsync } from './purity-modules/register-async.js' +export { debounce } from './purity-modules/debounce.js' +export { md5 } from './purity-modules/md5.js' +export { sanitize } from './purity-modules/sanitize.js' // Local: diff --git a/src/purity-modules/README.md b/src/purity-modules/README.md new file mode 100644 index 0000000..618d119 --- /dev/null +++ b/src/purity-modules/README.md @@ -0,0 +1,105 @@ +# Useful utils + +A handful of algorithms from different sources for using with **Purity** or without it. + +## MD5 hashing algorithm + +Borrowed [here](http://www.myersdaily.org/joseph/javascript/md5-text.html). + +Usage: + +```javascript +import { md5 } from 'https://tatomyr.github.io/purity/lib/md5.js' + +console.log(md5('some string')) +``` + +## Visibility Sensor + +Taken [here](https://vanillajstoolkit.com/helpers/isinviewport/) + +Usage: + +```javascript +import { trackVisibility } from 'https://tatomyr.github.io/purity/lib/visibility-sensor.js' + +… +trackVisibility($element, isInViewport => { + console.log($element + ' is in viewport:' + isInViewport) +}) +… +``` + + + +## Debounce + +Usage: + +```javascript +import { debounce } from 'https://tatomyr.github.io/purity/lib/debounce.js' + +… +render` + +` +… +``` + +Use a positive `timeout` for triggering the callback on the leading edge and a negative one for triggering the callback on the trailing edge. + +## Delay + +Usage: + +```javascript +import { delay } from 'https://tatomyr.github.io/purity/lib/delay.js' + +… +delay(