สิ่งที่ยังไม่ได้ทำ:
- ซ่อน element ที่มันเลยขอบเขตการแสดงผลออกไป เพื่อให้ลดภาระการโหลดหรือทำงานของเบราว์เซอร์ กรณีโหลดข้อมูลมามากๆมหาศาล
ScrollPagination.js Code (JavaScript)
/**
* Scroll pagination.
*
* @author Vee W.
* @license MIT.
*/
/**
* Scroll pagination (infinite scroll).
*
* @todo Hide overflow elements on top and bottom.
*/
class ScrollPagination {
/**
* Class constructor.
*
* @param {object} option
*/
constructor(option = {}) {
if (!option.containerSelector) {
option.containerSelector = '#container';
}
this.containerSelector = option.containerSelector;
if (!option.bottomOffset) {
// the bottom offset where scroll almost to very bottom of page.
option.bottomOffset = 30;
}
this.bottomOffset = option.bottomOffset;
// change url options ----------------------------
if (!option.changeURL) {
option.changeURL = true;
}
this.changeURL = option.changeURL;
if (!option.changeURLParamStartOffset) {
// querystring for start offset to push to the URL.
// example ?rdspStartOffset=10 when scroll to next page from first while displaying 10 items per page.
option.changeURLParamStartOffset = 'rdspStartOffset';
}
this.changeURLParamStartOffset = option.changeURLParamStartOffset;
// end change url options -------------------------
// ajax options -----------------------------------
if (!option.ajaxUrl) {
// set ajax url with `%startoffset%` to use as start offset.
// example: http://domain.tld/page?offset=%startoffset%
throw new Error('The `ajaxUrl` property is missing.');
}
this.ajaxUrl = option.ajaxUrl;
if (!option.ajaxMethod) {
option.ajaxMethod = 'GET';
}
this.ajaxMethod = option.ajaxMethod;
if (!option.ajaxData) {
// the ajax data to send with some methods such as POST, PATCH, PULL, DELETE, etc.
// the data will be like name=value&name2=value2 or get the data from the `FormData()` object.
// the string that contain `%startoffset%` will be replace with start offset.
option.ajaxData = '';
}
this.ajaxData = option.ajaxData;
if (!option.ajaxAccept) {
option.ajaxAccept = 'application/json';
}
this.ajaxAccept = option.ajaxAccept;
// response type for accept. possible values:
// text/html -> response
// application/xml -> responseXML
// application/json -> responseText
// text/plain -> responseText
// application/javascript, application/xxxscript -> responseText
if (!option.ajaxResponseAcceptType) {
option.ajaxResponseAcceptType = 'responseText';
}
this.ajaxResponseAcceptType = option.ajaxResponseAcceptType;
if (!option.ajaxContentType) {
option.ajaxContentType = 'application/x-www-form-urlencoded;charset=UTF-8';
}
this.ajaxContentType = option.ajaxContentType;
if (!option.ajaxDataSrc) {
// set data source for count how many items retrieved for set new start offset.
option.ajaxDataSrc = 'items';
}
this.ajaxDataSrc = option.ajaxDataSrc;
// end ajax options -------------------------------
this.currentStartOffset = this.detectAndSetCurrentStartOffset();
this.callingXHR = false;
this.isScrolling = '';// up or down.
this.XHR = new Promise((resolve, reject) => {});
}// constructor
/**
* AJAX pagination.
*
* @private This method was called from `checkScrollAndMakeXHR()`.
*/
ajaxPagination() {
let thisClass = this;
let promiseObj = new Promise((resolve, reject) => {
if (thisClass.callingXHR === true) {
return reject('previous XHR is calling.');
}
thisClass.callingXHR = true;
let XHR = new XMLHttpRequest();
XHR.addEventListener('error', (event) => {
thisClass.callingXHR = false;
reject({'response': '', 'status': (event.currentTarget ? event.currentTarget.status : ''), 'event': event});
});
XHR.addEventListener('loadstart', (event) => {
let response = (event.currentTarget ? event.currentTarget : event);
document.dispatchEvent(
new CustomEvent(
'rdScrollPagination.start', {'detail': response}
)
);
});
XHR.addEventListener('loadend', (event) => {
let response = (event.currentTarget ? event.currentTarget[thisClass.ajaxResponseAcceptType] : '');
if (thisClass.ajaxAccept.toLowerCase().includes('/json')) {
try {
if (response) {
response = JSON.parse(response);
}
} catch (exception) {
console.error(exception.message, response);
}
}
let headers = XHR.getAllResponseHeaders();
let headerMap = {};
if (headers) {
let headersArray = headers.trim().split(/[\r\n]+/);
headersArray.forEach((line) => {
let parts = line.split(': ');
let header = parts.shift();
let value = parts.join(': ');
headerMap[header] = value;
});
headersArray = undefined;
}
headers = undefined;
if (response[thisClass.ajaxDataSrc] && response[thisClass.ajaxDataSrc].length > 0) {
// if there are items after XHR.
// append pagination data element.
thisClass.appendPaginationDataElement();
// set next start offset.
thisClass.currentStartOffset = parseInt(thisClass.currentStartOffset) + parseInt(response[thisClass.ajaxDataSrc].length);
// mark calling to false to allow next pagination call.
thisClass.callingXHR = false;// move in here to prevent ajax call again when there are no more data.
}
if (event.currentTarget && event.currentTarget.status >= 100 && event.currentTarget.status < 400) {
resolve({'response': response, 'status': event.currentTarget.status, 'event': event, 'headers': headerMap});
} else {
reject({'response': response, 'status': event.currentTarget.status, 'event': event, 'headers': headerMap});
}
});
XHR.open(thisClass.ajaxMethod, thisClass.ajaxUrl.replace('%startoffset%', thisClass.currentStartOffset));
XHR.setRequestHeader('Accept', thisClass.ajaxAccept);
if (thisClass.ajaxContentType) {
XHR.setRequestHeader('Content-Type', thisClass.ajaxContentType);
}
XHR.send(thisClass.ajaxData.replace('%startoffset%', thisClass.currentStartOffset));
});
thisClass.XHR = promiseObj;
return promiseObj;
}// ajaxPagination
/**
* Append pagination data element.
*
* @private This method was called from `ajaxPagination()`.
*/
appendPaginationDataElement() {
let containerElement = document.querySelector(this.containerSelector);
if (containerElement) {
containerElement.insertAdjacentHTML(
'beforeend',
'<div class="rd-scroll-pagination"'
+ ' data-startoffset="' + this.currentStartOffset + '"'
//+ ' style="display:none;"'
+ '></div>'
);
}
}// appendPaginationDataElement
/**
* Check that scroll is near the display area and make XHR (AJAX).
*
* @private This method was called from `listenOnScroll()`.
* @param object event
*/
checkScrollAndMakeXHR(event) {
let thisClass = this;
let windowHeight = window.innerHeight;
let scrollTop = window.pageYOffset || document.documentElement.scrollTop;
let documentScrollHeight = document.documentElement.scrollHeight;
let totalScroll = (parseInt(windowHeight) + parseInt(scrollTop)) + parseInt(this.bottomOffset);
if (totalScroll >= documentScrollHeight) {
/*console.log(
'total scroll >= document scroll height.',
{
'totalScroll': totalScroll,
'documentScrollHeight': documentScrollHeight
}
);*/
// begins ajax pagination.
this.ajaxPagination()
.then((responseObject) => {
document.dispatchEvent(
new CustomEvent(
'rdScrollPagination.done', {'detail': responseObject}
)
);
thisClass.triggerOnScroll();
return Promise.resolve(responseObject);
})
.catch((responseObject) => {
// .catch() must be after .then(). see https://stackoverflow.com/a/42028776/128761
document.dispatchEvent(
new CustomEvent(
'rdScrollPagination.fail', {'detail': responseObject}
)
);
return Promise.reject(responseObject)
.then(() => {
// not called.
}, (responseObject) => {
// prevent uncaught error.
});
});
}
}// checkScrollAndMakeXHR
/**
* Check scrolling up or down and change current URL.
*
* @private This method was called from `listenOnScroll()`.
* @param {object} event
*/
checkScrollAndChangeURL(event) {
if (this.changeURL !== true) {
return ;
}
let thisClass = this;
let paginationDataElements = document.querySelectorAll('.rd-scroll-pagination');
if (paginationDataElements) {
paginationDataElements.forEach((item, index) => {
let rect = item.getBoundingClientRect();
if (rect.top >= 0 && rect.top < 30) {
// if scrolled and top of this pagination data element is on top within range (30 - for example).
// retrieve this start offset from `data-startoffset="n"`.
let thisStartOffset = item.dataset.startoffset;
// get all querystrings except start offset and re-assign the start offset.
const params = new URLSearchParams(window.location.search);
let paramObj = {};
for(let paramName of params.keys()) {
if (paramName !== thisClass.changeURLParamStartOffset) {
paramObj[paramName] = params.get(paramName);
}
}
paramObj[thisClass.changeURLParamStartOffset] = thisStartOffset;
// build querystring
let currentUrlNoQuerystring = window.location.href.split(/[?#]/)[0];
let queryString = Object.keys(paramObj).map((key) => {
return encodeURIComponent(key) + '=' + encodeURIComponent(paramObj[key])
}).join('&');
// replace current URL.
window.history.replaceState(null, '', currentUrlNoQuerystring + '?' + queryString);
return;
}
});
}
}// checkScrollAndChangeURL
/**
* Detect and set current start offset from querystring.
*
* @private This method was called from `constructor()`.
* @return int Return detected number of start offset.
*/
detectAndSetCurrentStartOffset() {
const params = new URLSearchParams(window.location.search);
let currentStartOffsetQuerystring = params.get(this.changeURLParamStartOffset);
if (
currentStartOffsetQuerystring === null ||
currentStartOffsetQuerystring === '' ||
isNaN(currentStartOffsetQuerystring) ||
isNaN(parseFloat(currentStartOffsetQuerystring)) ||
currentStartOffsetQuerystring < 0
) {
currentStartOffsetQuerystring = 0;
}
return parseInt(currentStartOffsetQuerystring);
}// detectAndSetCurrentStartOffset
/**
* Get XHR property object.
*
* @return XMLHttpRequest
*/
async getXHR() {
return this.XHR;
}// getXHR
/**
* Invoke, run the class.
*/
invoke() {
this.listenOnScroll();
}// invoke
/**
* Listen on scroll window/element.
*
* @private This method was called from `invoke()`.
*/
listenOnScroll() {
let thisClass = this;
let lastScroll = 0;
window.addEventListener('scroll', (event) => {
let scrollTop = window.pageYOffset || document.documentElement.scrollTop;
if (scrollTop >= lastScroll) {
// scrolling down
console.log('scrolling down');
thisClass.isScrolling = 'down';
thisClass.checkScrollAndMakeXHR(event);
thisClass.checkScrollAndChangeURL(event);
} else {
// scrolling up
console.log('scrolling up');
thisClass.isScrolling = 'up';
thisClass.checkScrollAndChangeURL(event);
}
lastScroll = (scrollTop <= 0 ? 0 : parseInt(scrollTop));
}, false);
this.triggerOnScroll();
}// listenOnScroll
/**
* Trigger scroll event,
* best on initialize the class to trigger event
* and make ajax call while next pagination element is near the display area.
*
* @private This method was called from `listenOnScroll()`.
*/
triggerOnScroll() {
window.dispatchEvent(new Event('scroll'));
}// triggerOnScroll
}// ScrollPagination