feat(xhr): implementing xhr client and some mobile optimizations
This commit is contained in:
@@ -175,11 +175,14 @@
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
min-width: 20%;
|
||||
max-width: 100%;
|
||||
background: $white;
|
||||
box-shadow: 0 .125em .25em 0 lighten($black, 60);
|
||||
z-index: 255;
|
||||
|
||||
margin-top: .5em;
|
||||
|
||||
border-radius: $border-radius;
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
@@ -190,6 +193,35 @@
|
||||
background: $white;
|
||||
box-shadow: none;
|
||||
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 {
|
||||
@@ -201,7 +233,7 @@
|
||||
&-item, &-empty {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
padding: $spacer * .5 $spacer;
|
||||
padding: $spacer;
|
||||
color: $text-secondary;
|
||||
font-weight: 500;
|
||||
&:not(:last-child) {
|
||||
@@ -321,7 +353,7 @@
|
||||
|
||||
.simplecomplete__results-list {
|
||||
padding-top: 1em;
|
||||
&-empty {
|
||||
&-empty, &-item {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -331,8 +363,8 @@
|
||||
border-radius: $border-radius;
|
||||
height: 1.2em;
|
||||
background: $light-gray;
|
||||
width: 80%;
|
||||
margin: 0 1em;
|
||||
width: 90%;
|
||||
margin: 0 $spacer * .5;
|
||||
animation-name: pulsingAnim;
|
||||
animation-iteration-count: infinite;
|
||||
animation-duration: .8s;
|
||||
@@ -342,6 +374,7 @@
|
||||
&:after {
|
||||
width: 60%;
|
||||
margin-top: .6em;
|
||||
margin-bottom: .4em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +26,13 @@ const defaultOptions = {
|
||||
reset () {}, // user resets the element to its initial state
|
||||
|
||||
// data handling
|
||||
onBeforeRequest () {}, // before the request to the url is sent
|
||||
onRequest () {},
|
||||
onResponse () {}, // server sends a response
|
||||
onErrorResponse () {}, // server error
|
||||
onParseItem () {}, // parses an item from response
|
||||
onBeforeRequest (xhr, headers, urlParams, formData) {}, // before the request to the url is sent
|
||||
onResponse (data, xhr) {}, // server sends a response
|
||||
onTimeout (xhr) {}, // if the request times out
|
||||
onErrorResponse (err, xhr) {}, // server error
|
||||
onParseResponse () {}, // parses an item from response
|
||||
onParseItems (data) { data = [] },
|
||||
onParseItem (data) {},
|
||||
onRenderItem (out, data) { return out }, // renders an item from response
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ export default class SimpleComplete {
|
||||
ENTER: 13,
|
||||
TAB: 9
|
||||
}
|
||||
|
||||
static client = new XMLHttpRequest()
|
||||
|
||||
static initClass () {
|
||||
this.prototype.events = []
|
||||
|
||||
@@ -162,22 +165,120 @@ export default class SimpleComplete {
|
||||
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) {
|
||||
if (!this.query || (this.options.minLength > 0 && this.query.length < this.options.minLength)) {
|
||||
this.onError('simplecomplete.errors.min-length')
|
||||
} else {
|
||||
if (this.wrapper.classList.contains('is-busy')) {
|
||||
this.queued = true
|
||||
} else {
|
||||
console.log('TODO: send request')
|
||||
} else if (this.options.url) {
|
||||
this.wrapper.classList.add('is-busy')
|
||||
window.setTimeout(() => {
|
||||
|
||||
this.sendRequest((xhr) => {
|
||||
console.log('DONE', xhr)
|
||||
})
|
||||
|
||||
/*window.setTimeout(() => {
|
||||
this.wrapper.classList.remove('is-busy')
|
||||
if(this.queued) {
|
||||
this.queued = false
|
||||
this.onSend ()
|
||||
}
|
||||
}, 3000)
|
||||
}, 3000)*/
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -322,21 +423,75 @@ export default class SimpleComplete {
|
||||
}
|
||||
|
||||
onResults (data = null) {
|
||||
try {
|
||||
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) {
|
||||
this.resultsList.innerHTML = ''
|
||||
|
||||
try {
|
||||
this.results.filter(res => {
|
||||
return !this.query || res.value.toString().toLowerCase().indexOf(this.query.toLowerCase()) >= 0 || res.label.toLowerCase().indexOf(this.query.toLowerCase()) >= 0
|
||||
}).forEach(res => {
|
||||
this.resultsList.appendChild(this.renderOption(res))
|
||||
if (this.isSelectElement ()) {
|
||||
[...this.element.options].forEach(option => {
|
||||
this.resultsList.appendChild(this.renderOption({
|
||||
value: option.value,
|
||||
label: option.textContent
|
||||
}))
|
||||
})
|
||||
} catch(err) {
|
||||
console.log(err)
|
||||
} else {
|
||||
this.results.forEach( res => this.resultsList.appendChild(this.renderOption(res)) )
|
||||
}
|
||||
|
||||
if (!this.resultsList.childNodes.length) {
|
||||
@@ -349,6 +504,9 @@ export default class SimpleComplete {
|
||||
this.resultsList.appendChild(emptyNode)
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
this.onError(err)
|
||||
}
|
||||
}
|
||||
|
||||
isSelected (_val) {
|
||||
@@ -439,9 +597,10 @@ SimpleComplete.discover = () => {
|
||||
|
||||
if (inputs && inputs.length > 0) {
|
||||
inputs.forEach(_in => {
|
||||
console.log(_in, _in.dataset.blankable || false)
|
||||
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
|
||||
}
|
||||
res.push(new SimpleComplete(_in, options));
|
||||
|
||||
Reference in New Issue
Block a user