feat(xhr): implementing xhr client and some mobile optimizations

This commit is contained in:
Benjamin Wegener
2021-10-10 20:21:09 +02:00
parent 5195ebb4a1
commit 5b93a984c0
3 changed files with 228 additions and 34 deletions

View File

@@ -175,11 +175,14 @@
position: absolute; position: absolute;
top: 100%; top: 100%;
left: 0; left: 0;
min-width: 20%;
max-width: 100%; max-width: 100%;
background: $white; background: $white;
box-shadow: 0 .125em .25em 0 lighten($black, 60); box-shadow: 0 .125em .25em 0 lighten($black, 60);
z-index: 255; z-index: 255;
margin-top: .5em;
border-radius: $border-radius; border-radius: $border-radius;
@media screen and (max-width: 992px) { @media screen and (max-width: 992px) {
@@ -190,6 +193,35 @@
background: $white; background: $white;
box-shadow: none; box-shadow: none;
border-radius: 0; border-radius: 0;
margin-top: 0;
}
@media screen and (min-width: 993px) {
&:before {
content: "";
position: absolute;
left: 50%;
bottom: 100%;
z-index: -10;
border: solid .5em $white;
border-bottom-color: transparent;
border-left-color: transparent;
transform: translate(-50%, 50%) rotate(-45deg);
box-shadow: 0 0 .25em $default-shadow;
}
&:after {
content: "";
position: absolute;
top: 0;
left: 50%;
width: 4em;
height: 2em;
z-index: -1;
background: $white;
transform: translateX(-50%);
}
} }
&-list { &-list {
@@ -201,7 +233,7 @@
&-item, &-empty { &-item, &-empty {
cursor: pointer; cursor: pointer;
display: block; display: block;
padding: $spacer * .5 $spacer; padding: $spacer;
color: $text-secondary; color: $text-secondary;
font-weight: 500; font-weight: 500;
&:not(:last-child) { &:not(:last-child) {
@@ -321,7 +353,7 @@
.simplecomplete__results-list { .simplecomplete__results-list {
padding-top: 1em; padding-top: 1em;
&-empty { &-empty, &-item {
display: none; display: none;
} }
@@ -331,8 +363,8 @@
border-radius: $border-radius; border-radius: $border-radius;
height: 1.2em; height: 1.2em;
background: $light-gray; background: $light-gray;
width: 80%; width: 90%;
margin: 0 1em; margin: 0 $spacer * .5;
animation-name: pulsingAnim; animation-name: pulsingAnim;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-duration: .8s; animation-duration: .8s;
@@ -342,6 +374,7 @@
&:after { &:after {
width: 60%; width: 60%;
margin-top: .6em; margin-top: .6em;
margin-bottom: .4em;
} }
} }
} }

View File

@@ -26,11 +26,13 @@ const defaultOptions = {
reset () {}, // user resets the element to its initial state reset () {}, // user resets the element to its initial state
// data handling // data handling
onBeforeRequest () {}, // before the request to the url is sent onBeforeRequest (xhr, headers, urlParams, formData) {}, // before the request to the url is sent
onRequest () {}, onResponse (data, xhr) {}, // server sends a response
onResponse () {}, // server sends a response onTimeout (xhr) {}, // if the request times out
onErrorResponse () {}, // server error onErrorResponse (err, xhr) {}, // server error
onParseItem () {}, // parses an item from response onParseResponse () {}, // parses an item from response
onParseItems (data) { data = [] },
onParseItem (data) {},
onRenderItem (out, data) { return out }, // renders an item from response onRenderItem (out, data) { return out }, // renders an item from response
} }

View File

@@ -6,6 +6,9 @@ export default class SimpleComplete {
ENTER: 13, ENTER: 13,
TAB: 9 TAB: 9
} }
static client = new XMLHttpRequest()
static initClass () { static initClass () {
this.prototype.events = [] this.prototype.events = []
@@ -162,22 +165,120 @@ export default class SimpleComplete {
this.onResults() this.onResults()
} }
finishRequest (xhr) {
this.wrapper.classList.remove('is-busy')
}
sendRequest (_cb = null) {
let headers = {
Accept: 'application/json',
'Cache-Control': 'no-cache',
'X-Requested-With': 'XMLHttpRequest',
...(this.options.headers || {})
}
let params = {
...(this.options.params || {})
}
params[this.options.paramName || 'q'] = this.query
let formData = new FormData()
let urlParams = new URLSearchParams()
let target = this.options.url
let xhr = SimpleComplete.client
xhr.abort()
xhr.addEventListener('load', e => {
this.finishRequest (xhr)
this.handleResponse (xhr, params, formData, e)
})
xhr.addEventListener('timeout', (e) => {
this.finishRequest (xhr)
this.options.onTimeout (xhr)
})
xhr.addEventListener('error', (e) => {
this.finishRequest (xhr)
this.options.onErrorResponse (e, xhr)
})
Object.keys(params).forEach(_key => {
urlParams.set(_key, params[_key])
formData.append(_key, params[_key])
})
this.options.onBeforeRequest(xhr, headers, urlParams, formData)
target += `?${urlParams.toString()}`
xhr.open(
this.options.method || 'GET',
target
)
if (xhr.readyState != 1) {
console.log(xhr.readyState)
this.onError('xhr_not_ready')
return;
}
Object.keys(headers).forEach(_key => xhr.setRequestHeader(_key, headers[_key]))
SimpleComplete.timeout = (this.options.timeout || 30) * 1000
xhr.send(formData)
}
handleResponse (xhr, params, formData, e) {
let response = xhr.responseText || null
let err = null
if (response && xhr.getResponseHeader('Content-Type').includes("application/json")) {
try {
response = JSON.parse(response)
this.options.onParseResponse(response)
if (!Array.isArray(response)) {
throw new Error('simplecomplete.errors.no_array_response')
} else {
console.log(response)
}
} catch(_e) {
err = _e
this.options.onParseResponse(response)
}
}
if (!(200 <= xhr.status && xhr.status < 300)) {
this.onError('simplecomplete.errors.xhr_wrong_status')
} else {
let data =
this.options.onResponse (response, xhr)
this.onResults(response.data || response)
}
}
onSend (e = null) { onSend (e = null) {
if (!this.query || (this.options.minLength > 0 && this.query.length < this.options.minLength)) { if (!this.query || (this.options.minLength > 0 && this.query.length < this.options.minLength)) {
this.onError('simplecomplete.errors.min-length') this.onError('simplecomplete.errors.min-length')
} else { } else {
if (this.wrapper.classList.contains('is-busy')) { if (this.wrapper.classList.contains('is-busy')) {
this.queued = true this.queued = true
} else { } else if (this.options.url) {
console.log('TODO: send request')
this.wrapper.classList.add('is-busy') this.wrapper.classList.add('is-busy')
window.setTimeout(() => {
this.sendRequest((xhr) => {
console.log('DONE', xhr)
})
/*window.setTimeout(() => {
this.wrapper.classList.remove('is-busy') this.wrapper.classList.remove('is-busy')
if(this.queued) { if(this.queued) {
this.queued = false this.queued = false
this.onSend () this.onSend ()
} }
}, 3000) }, 3000)*/
} }
} }
} }
@@ -322,21 +423,75 @@ export default class SimpleComplete {
} }
onResults (data = null) { onResults (data = null) {
try {
if(data !== null) { if(data !== null) {
this.results = data if (!Array.isArray(data)) {
this.onError('simplecomplete.errors.data_invalid')
return
}
this.options.onParseItems (data)
this.results = data.map(res => {
let item = this.options.onParseItem(res) || res
switch(typeof item) {
case 'string':
item = {
value: item,
label: item
}
break
case 'object':
item = {
value: item.id || item.value,
label: item.label || item.name
}
break
default:
if (Array.isArray(item) && item.length > 0) {
item = {
value: item[0],
label: item[item.length - 1]
}
} else {
throw new Error('simplecomplete.errors.invalid response item')
}
break
}
return item
})
if (this.results && this.isSelectElement ()) {
[...this.element.options].forEach(option => {
if (!this.isSelected(option.value)) {
option.remove()
}
})
this.results.forEach(item => {
let option = SimpleComplete.createElement('OPTION')
option.value = item.value || item.id
option.textContent = item.label || item.name || item
if (!this.isSelected(option.value)) {
this.element.appendChild(option)
}
})
}
} }
if(this.results && this.resultsList) { if(this.results && this.resultsList) {
this.resultsList.innerHTML = '' this.resultsList.innerHTML = ''
if (this.isSelectElement ()) {
try { [...this.element.options].forEach(option => {
this.results.filter(res => { this.resultsList.appendChild(this.renderOption({
return !this.query || res.value.toString().toLowerCase().indexOf(this.query.toLowerCase()) >= 0 || res.label.toLowerCase().indexOf(this.query.toLowerCase()) >= 0 value: option.value,
}).forEach(res => { label: option.textContent
this.resultsList.appendChild(this.renderOption(res)) }))
}) })
} catch(err) { } else {
console.log(err) this.results.forEach( res => this.resultsList.appendChild(this.renderOption(res)) )
} }
if (!this.resultsList.childNodes.length) { if (!this.resultsList.childNodes.length) {
@@ -349,6 +504,9 @@ export default class SimpleComplete {
this.resultsList.appendChild(emptyNode) this.resultsList.appendChild(emptyNode)
} }
} }
} catch(err) {
this.onError(err)
}
} }
isSelected (_val) { isSelected (_val) {
@@ -439,9 +597,10 @@ SimpleComplete.discover = () => {
if (inputs && inputs.length > 0) { if (inputs && inputs.length > 0) {
inputs.forEach(_in => { inputs.forEach(_in => {
console.log(_in, _in.dataset.blankable || false)
let options = { let options = {
minLength: _in.dataset.minLength || null, url: _in.dataset.sc || null,
paramName: _in.dataset.param || _defaults.paramName,
minLength: _in.dataset.minLength !== null ? _in.dataset.minLength : null,
blankable: _in.dataset.blankable || false blankable: _in.dataset.blankable || false
} }
res.push(new SimpleComplete(_in, options)); res.push(new SimpleComplete(_in, options));