@zestia/ember-simple-infinite-scroller
Advanced tools
Comparing version 7.0.5 to 8.0.0-beta.0
import Component from '@glimmer/component'; | ||
import { debounce, cancel, scheduleOnce } from '@ember/runloop'; | ||
import { action } from '@ember/object'; | ||
import { resolve } from 'rsvp'; | ||
import { inject } from '@ember/service'; | ||
import { tracked } from '@glimmer/tracking'; | ||
const { round } = Math; | ||
export default class InfiniteScrollerComponent extends Component { | ||
@inject('-infinite-scroller') _infiniteScroller; | ||
debug = false; | ||
scroller = null; | ||
debounceId = null; | ||
@tracked error = null; | ||
@tracked isLoading = false; | ||
@tracked isScrollable = false; | ||
element = null; | ||
get selector() { | ||
return this.args.selector || null; | ||
get debounce() { | ||
return this.args.debounce ?? 100; | ||
} | ||
get useDocument() { | ||
return this.args.useDocument || false; | ||
get percent() { | ||
return this.args.percent ?? 100; | ||
} | ||
get scrollDebounce() { | ||
return this.args.scrollDebounce || 100; | ||
get normalisedScrollerElement() { | ||
if (this.scroller instanceof Document) { | ||
return this.scroller.documentElement; | ||
} else { | ||
return this.scroller; | ||
} | ||
} | ||
get leeway() { | ||
return parseInt(this.args.leeway || '0%', 10); | ||
} | ||
@action | ||
handleInsertElement(element) { | ||
this._registerElement(element); | ||
if (!this.scroller) { | ||
this._registerScroller(this.args.element ?? element); | ||
} | ||
this._scheduleCheckScrollable(); | ||
this._listen(); | ||
} | ||
@@ -41,4 +42,3 @@ | ||
handleDestroyElement() { | ||
this._stopListening(); | ||
this._deregisterElement(); | ||
this._deregisterScroller(); | ||
} | ||
@@ -51,55 +51,44 @@ | ||
_registerElement(element) { | ||
this.element = element; | ||
@action | ||
setElement(element) { | ||
this._registerScroller(element); | ||
} | ||
_deregisterElement() { | ||
this.element = null; | ||
} | ||
_isScrollable() { | ||
let element = this._scroller(); | ||
if (this.useDocument) { | ||
element = this._documentElement(); | ||
_registerScroller(element) { | ||
if (this.scroller) { | ||
this._stopListening(); | ||
} | ||
if (!element) { | ||
return; | ||
} | ||
this.scroller = element; | ||
return element.scrollHeight > element.clientHeight; | ||
this._startListening(); | ||
} | ||
_scheduleCheckScrollable() { | ||
scheduleOnce('afterRender', this, '_checkScrollable'); | ||
_deregisterScroller() { | ||
this._stopListening(); | ||
cancel(this.debounceId); | ||
this.scroller = null; | ||
} | ||
_checkScrollable() { | ||
this.isScrollable = this._isScrollable(); | ||
} | ||
_startListening() { | ||
this._scrollHandler = this._handleScroll.bind(this); | ||
_listen() { | ||
this._scrollHandler = this._scroll.bind(this); | ||
this._listener().addEventListener('scroll', this._scrollHandler); | ||
this.scroller.addEventListener('scroll', this._scrollHandler); | ||
} | ||
_stopListening() { | ||
this._listener().removeEventListener('scroll', this._scrollHandler); | ||
cancel(this._scrollDebounceId); | ||
this.scroller.removeEventListener('scroll', this._scrollHandler); | ||
} | ||
_scroll(e) { | ||
this._scrollDebounceId = debounce( | ||
this, | ||
'_debouncedScroll', | ||
e, | ||
this.scrollDebounce | ||
); | ||
_handleScroll() { | ||
this.debounceId = debounce(this, '_checkShouldLoadMore', this.debounce); | ||
} | ||
_debouncedScroll() { | ||
if (this._shouldLoadMore()) { | ||
_checkShouldLoadMore() { | ||
const scrollState = this._getScrollState(); | ||
const shouldLoadMore = scrollState.reachedBottom && !this.isLoading; | ||
this._debug({ ...scrollState, shouldLoadMore }); | ||
if (shouldLoadMore) { | ||
this._loadMore(); | ||
@@ -109,84 +98,45 @@ } | ||
_log() { | ||
this._infiniteScroller.log(...arguments); | ||
} | ||
_checkScrollable() { | ||
if (!this.scroller) { | ||
return; | ||
} | ||
_document() { | ||
return this._infiniteScroller.document; | ||
} | ||
const scrollState = this._getScrollState(); | ||
_documentElement() { | ||
return this._infiniteScroller.documentElement; | ||
} | ||
this._debug({ ...scrollState }); | ||
_listener() { | ||
if (this.useDocument) { | ||
return this._document(); | ||
} else { | ||
return this._scroller(); | ||
} | ||
this.isScrollable = scrollState.isScrollable; | ||
} | ||
_scroller() { | ||
if (this.selector) { | ||
return this.element.querySelector(this.selector); | ||
} else { | ||
return this.element; | ||
} | ||
_scheduleCheckScrollable() { | ||
scheduleOnce('afterRender', this, '_checkScrollable'); | ||
} | ||
_shouldLoadMore() { | ||
let state; | ||
if (this.useDocument) { | ||
state = this._detectBottomOfElementInDocument(); | ||
} else { | ||
state = this._detectBottomOfElement(); | ||
_debug(state) { | ||
if (!this.debug) { | ||
return; | ||
} | ||
state.shouldLoadMore = state.reachedBottom && !this.isLoading; | ||
this._log(state); | ||
return state.shouldLoadMore; | ||
console.table([state]); // eslint-disable-line | ||
} | ||
_detectBottomOfElementInDocument() { | ||
const scroller = this._scroller(); | ||
const clientHeight = this._infiniteScroller.documentElement.clientHeight; | ||
const bottom = scroller.getBoundingClientRect().bottom; | ||
const leeway = this.leeway; | ||
const pixelsToBottom = bottom - clientHeight; | ||
const percentageToBottom = (pixelsToBottom / bottom) * 100; | ||
const reachedBottom = percentageToBottom <= leeway; | ||
return { | ||
clientHeight, | ||
bottom, | ||
leeway, | ||
pixelsToBottom, | ||
percentageToBottom, | ||
reachedBottom | ||
}; | ||
} | ||
_detectBottomOfElement() { | ||
const scroller = this._scroller(); | ||
const scrollHeight = scroller.scrollHeight; | ||
const scrollTop = scroller.scrollTop; | ||
const clientHeight = scroller.clientHeight; | ||
_getScrollState() { | ||
const element = this.normalisedScrollerElement; | ||
const scrollHeight = element.scrollHeight; | ||
const scrollTop = element.scrollTop; | ||
const clientHeight = element.clientHeight; | ||
const isScrollable = scrollHeight > clientHeight; | ||
const bottom = scrollHeight - clientHeight; | ||
const leeway = this.leeway; | ||
const pixelsToBottom = bottom - scrollTop; | ||
const percentageToBottom = (pixelsToBottom / bottom) * 100; | ||
const reachedBottom = percentageToBottom <= leeway; | ||
const percent = this.percent; | ||
const percentScrolled = round((scrollTop / bottom) * 100); | ||
const reachedBottom = percentScrolled >= percent; | ||
return { | ||
isScrollable, | ||
scrollHeight, | ||
clientHeight, | ||
scrollTop, | ||
clientHeight, | ||
bottom, | ||
leeway, | ||
pixelsToBottom, | ||
percentageToBottom, | ||
percent, | ||
percentScrolled, | ||
reachedBottom | ||
@@ -196,22 +146,7 @@ }; | ||
_loadMore() { | ||
const action = this.args.onLoadMore; | ||
if (typeof action !== 'function') { | ||
return; | ||
} | ||
this.error = null; | ||
async _loadMore() { | ||
this.isLoading = true; | ||
resolve(action()) | ||
.catch(this._loadError.bind(this)) | ||
.finally(this._loadFinished.bind(this)); | ||
} | ||
await this._invokeAction('onLoadMore'); | ||
_loadError(error) { | ||
this.error = error; | ||
} | ||
_loadFinished() { | ||
this.isLoading = false; | ||
@@ -221,2 +156,10 @@ | ||
} | ||
_invokeAction(name, ...args) { | ||
const action = this.args[name]; | ||
if (typeof action === 'function') { | ||
return action(...args); | ||
} | ||
} | ||
} |
# Changelog | ||
## 8.0.0-beta.0 | ||
- Removes `@selector` in favour of `@element` | ||
- Removes `@useDocument` in favour of `@element` | ||
- Removes `scroller.error` | ||
- Renames `@scrollDebounce` to `@debounce` | ||
- Renames `@leeway` to `@percent`. This is the inverse! | ||
- Adds `scroller.setElement` to make setting child elements easier | ||
- Upgrades dependencies | ||
## 7.0.7 | ||
- Fix division by zero | ||
## 7.0.6 | ||
- Run ember-cli-update | ||
## 7.0.5 | ||
@@ -4,0 +22,0 @@ |
{ | ||
"name": "@zestia/ember-simple-infinite-scroller", | ||
"version": "7.0.5", | ||
"version": "8.0.0-beta.0", | ||
"description": "Simple infinite scroller component for Ember apps", | ||
@@ -34,10 +34,11 @@ "directories": { | ||
"@ember/optional-features": "^2.0.0", | ||
"@zestia/ember-template-lint-plugin": "^3.0.7", | ||
"@zestia/eslint-config": "^3.0.2", | ||
"@ember/test-helpers": "^2.1.3", | ||
"@zestia/ember-template-lint-plugin": "^3.0.9", | ||
"@zestia/eslint-config": "^3.0.3", | ||
"@zestia/prettier-config": "^1.0.4", | ||
"@zestia/stylelint-config": "^2.0.44", | ||
"@zestia/stylelint-config": "^2.0.45", | ||
"babel-eslint": "^10.1.0", | ||
"broccoli-asset-rev": "^3.0.0", | ||
"ember-auto-import": "^1.7.0", | ||
"ember-cli": "^3.22.0", | ||
"ember-auto-import": "^1.10.0", | ||
"ember-cli": "^3.23.0", | ||
"ember-cli-dependency-checker": "^3.2.0", | ||
@@ -47,25 +48,26 @@ "ember-cli-github-pages": "^0.2.2", | ||
"ember-cli-sri": "^2.1.1", | ||
"ember-cli-uglify": "^3.0.0", | ||
"ember-cli-terser": "^4.0.0", | ||
"ember-disable-prototype-extensions": "^1.1.3", | ||
"ember-load-initializers": "^2.1.1", | ||
"ember-load-initializers": "^2.1.2", | ||
"ember-maybe-import-regenerator": "^0.1.6", | ||
"ember-qunit": "^4.6.0", | ||
"ember-qunit": "^5.1.1", | ||
"ember-resolver": "^8.0.2", | ||
"ember-source": "^3.22.0", | ||
"ember-source": "^3.23.1", | ||
"ember-source-channel-url": "^3.0.0", | ||
"ember-template-lint": "^2.14.0", | ||
"ember-template-lint": "^2.15.0", | ||
"ember-try": "^1.4.0", | ||
"eslint": "^7.12.1", | ||
"eslint": "^7.15.0", | ||
"loader.js": "^4.7.0", | ||
"npm-run-all": "^4.1.5", | ||
"prettier": "^2.1.2", | ||
"qunit-dom": "^1.5.0", | ||
"release-it": "^14.2.1", | ||
"sass": "^1.29.0", | ||
"stylelint": "^13.7.2" | ||
"prettier": "^2.2.1", | ||
"qunit": "^2.13.0", | ||
"qunit-dom": "^1.6.0", | ||
"release-it": "^14.2.2", | ||
"sass": "^1.30.0", | ||
"stylelint": "^13.8.0" | ||
}, | ||
"dependencies": { | ||
"@ember/render-modifiers": "^1.0.2", | ||
"@glimmer/component": "^1.0.2", | ||
"@glimmer/tracking": "^1.0.2", | ||
"@glimmer/component": "^1.0.3", | ||
"@glimmer/tracking": "^1.0.3", | ||
"ember-cli-babel": "^7.23.0", | ||
@@ -72,0 +74,0 @@ "ember-cli-htmlbars": "^5.3.1" |
@@ -52,2 +52,3 @@ # @zestia/ember-simple-infinite-scroller | ||
- Supports use with FastBoot ✔︎ | ||
- No included styles ✔︎ | ||
@@ -64,25 +65,20 @@ ## Configuration | ||
<td>onLoadMore</td> | ||
<td>Action to perform when the bottom is scrolled into view</td> | ||
<td>Action to perform when the <code>@percent</code> is scrolled past</td> | ||
<td><code>null</code></td> | ||
</tr> | ||
<tr> | ||
<td>selector</td> | ||
<td>Monitors the scrolling of a specific child element, e.g. <code>selector=".foo-bar"</code></td> | ||
<td>element</td> | ||
<td>Monitors the scroll position of the given element</td> | ||
<td><code>null</code></td> | ||
</tr> | ||
<tr> | ||
<td>useDocument</td> | ||
<td>Monitors the document scroll position rather than the element's scroll position.</td> | ||
<td><code>false</code></td> | ||
<td>percent</td> | ||
<td>Distance scroll from the top for when to fire the load more action</td> | ||
<td><code>100</code></td> | ||
</tr> | ||
<tr> | ||
<td>leeway</td> | ||
<td>Percentage distance away from the bottom</td> | ||
<td><code>"0%"</code></td> | ||
<td>debounce</td> | ||
<td>Milliseconds delay for when to check if more needs to be loaded</td> | ||
<td><code>100</code></td> | ||
</tr> | ||
<tr> | ||
<td>scrollDebounce</td> | ||
<td>Milliseconds delay used to check if the bottom has been reached</td> | ||
<td><code>100</code> ms</td> | ||
</tr> | ||
</table> | ||
@@ -100,2 +96,6 @@ | ||
<tr> | ||
<td>setElement</td> | ||
<td>Sets the element for which to monitor the scroll position of</td> | ||
</tr> | ||
<tr> | ||
<td>isLoading</td> | ||
@@ -109,6 +109,2 @@ <td>True when the promise for more data has not resolved yet</td> | ||
<tr> | ||
<td>error</td> | ||
<td>The caught error from the last attempt to load more</td> | ||
</tr> | ||
<tr> | ||
<td>loadMore</td> | ||
@@ -119,17 +115,2 @@ <td>Action for manually loading more</td> | ||
## Element vs Document scroll | ||
Either make your component scrollable: | ||
```css | ||
.my-element { | ||
max-height: 300px; | ||
overflow-y: auto; | ||
} | ||
``` | ||
**OR** | ||
Set `@useDocument={{true}}` if your component is not scrollable. | ||
## Performance | ||
@@ -142,3 +123,3 @@ | ||
```javascript | ||
customEvents: { | ||
customEvents = { | ||
touchstart: null, | ||
@@ -148,9 +129,9 @@ touchmove: null, | ||
touchcancel: null | ||
} | ||
}; | ||
``` | ||
## Other scenarios | ||
## Scenario to be aware of | ||
If your scrollable element is displaying 10 things, but they don't cause the element to overflow, | ||
then the user won't ever be able to load more - because they won't be able to scroll and therefore | ||
then the user won't ever be able to load more - because they won't be able to _scroll_ and therefore | ||
the `onLoadMore` action will never fire. | ||
@@ -166,10 +147,6 @@ | ||
{{#if this.hasMoreThings}} | ||
{{#if scroller.isScrollable}} | ||
Loading more... | ||
{{else}} | ||
<button {{on "click" scroller.loadMore}}>Load more</button> | ||
{{/if}} | ||
{{/if}} | ||
{{#unless scroller.isScrollable}} | ||
<button {{on "click" scroller.loadMore}}>Load more</button> | ||
{{/unless}} | ||
</InfiniteScroller> | ||
``` |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
15453
33
15
143
1
145
1
Updated@glimmer/component@^1.0.3
Updated@glimmer/tracking@^1.0.3