feat(prototype): the first real 💩 prototype version
this is the first real working interface with almost what i imagined it to be like. Next: xhr implementation with requesting sources to update the possible choices
This commit is contained in:
2
src/.gitignore
vendored
Normal file
2
src/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*.css
|
||||||
|
*.map
|
||||||
22
src/scss/_variables.scss
Normal file
22
src/scss/_variables.scss
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
$spacer: 1em !default;
|
||||||
|
|
||||||
|
$black: #111111 !default;
|
||||||
|
$white: #FFFFFF !default;
|
||||||
|
$gray: #DDDDDD !default;
|
||||||
|
$light-gray: #EFEFEF !default;
|
||||||
|
$yellow: #FFFE47 !default;
|
||||||
|
|
||||||
|
$primary: #FF64B1 !default;
|
||||||
|
$secondary: #EFEFEF !default;
|
||||||
|
|
||||||
|
$text: $black !default;
|
||||||
|
$text-secondary: #BBBBBB !default;
|
||||||
|
|
||||||
|
$default-shadow: rgba(0,0,0,.3);
|
||||||
|
|
||||||
|
$border-default: solid 0.125 * $spacer $default-shadow !default;
|
||||||
|
$border-default-transparent: solid 0.0625 * $spacer transparent !default;
|
||||||
|
$border-radius: .5em !default;
|
||||||
|
|
||||||
|
$padding: $spacer * .5 $spacer * .75 !default;
|
||||||
|
|
||||||
354
src/scss/simplecomplete.scss
Normal file
354
src/scss/simplecomplete.scss
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
@use "sass:math";
|
||||||
|
@import 'variables';
|
||||||
|
|
||||||
|
@keyframes pulsingAnim {
|
||||||
|
0% {
|
||||||
|
opacity: .6;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: .6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scrollingBg {
|
||||||
|
0% {
|
||||||
|
background-position: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: -100% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes highlightAnim {
|
||||||
|
0% {
|
||||||
|
background: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
15% {
|
||||||
|
background: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
30% {
|
||||||
|
background: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
background: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplecomplete {
|
||||||
|
position: relative;
|
||||||
|
border: $border-default;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
background: linear-gradient(90deg, $white 0, $white 100%);
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: background 3s;
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
&-field {
|
||||||
|
flex: 1;
|
||||||
|
background: white;
|
||||||
|
padding: $padding;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: $border-default-transparent;
|
||||||
|
border-radius: $border-radius * .8;
|
||||||
|
outline: none;
|
||||||
|
margin: .125em;
|
||||||
|
|
||||||
|
@media screen and (max-width: 992px) {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-cancel {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 0;
|
||||||
|
width: 2em;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2em;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
display: inline-block;
|
||||||
|
content: "\00D7";
|
||||||
|
color: darken($gray, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 992px) {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
transform: none;
|
||||||
|
top: auto;
|
||||||
|
right: auto;
|
||||||
|
|
||||||
|
background: $white;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
display: block;
|
||||||
|
padding: 0 $spacer;
|
||||||
|
color: $gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
@extend .simplecomplete__input-field;
|
||||||
|
&:not(.hide) {
|
||||||
|
& ~ .simplecomplete__input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .selected-option, > .empty-item {
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: $padding;
|
||||||
|
margin: 0 .125em;
|
||||||
|
background: $light-gray;
|
||||||
|
color: $text-secondary;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
border: $border-default-transparent;
|
||||||
|
background: $gray;
|
||||||
|
color: $black;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: -.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: -.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .empty-item {
|
||||||
|
background: $white;
|
||||||
|
color: $text-secondary;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-empty {
|
||||||
|
color: $text-secondary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-multiple, &.is-blankable {
|
||||||
|
.simplecomplete__value > .selected-option {
|
||||||
|
&:after {
|
||||||
|
content: "\00D7";
|
||||||
|
display: inline;
|
||||||
|
margin-left: .25em;
|
||||||
|
color: darken($text-secondary, 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplecomplete__input-cancel {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__results {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
background: $white;
|
||||||
|
box-shadow: 0 .125em .25em 0 lighten($black, 60);
|
||||||
|
z-index: 255;
|
||||||
|
|
||||||
|
border-radius: $border-radius;
|
||||||
|
|
||||||
|
@media screen and (max-width: 992px) {
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: $white;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-list {
|
||||||
|
border-radius: $border-radius;
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 30vh;
|
||||||
|
padding: $spacer * .5;
|
||||||
|
&-item, &-empty {
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
padding: $spacer * .5 $spacer;
|
||||||
|
color: $text-secondary;
|
||||||
|
font-weight: 500;
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: solid 1px lighten($black, 90);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $black;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .highlight {
|
||||||
|
display: inline;
|
||||||
|
color: $primary;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
font-weight: 500;
|
||||||
|
background: $primary;
|
||||||
|
color: $white;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: darken($primary, 10);
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .highlight {
|
||||||
|
color: $text;
|
||||||
|
background: $yellow;
|
||||||
|
padding: $spacer * .125 $spacer * .25;
|
||||||
|
margin: -$spacer * .125 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
border-radius: $border-radius;
|
||||||
|
background: $light-gray;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 992px) {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-blankable:not(.active) {
|
||||||
|
.simplecomplete__value > .selected-option {
|
||||||
|
&:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active:not(.is-select) {
|
||||||
|
.simplecomplete__results {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active.is-select, &:not(.is-select) {
|
||||||
|
.simplecomplete__input-field {
|
||||||
|
padding: $spacer * 1.05;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.inline {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: $primary;
|
||||||
|
@media screen and (max-width: 992px) {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
border-color: transparent;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
background: $gray;
|
||||||
|
z-index: 255;
|
||||||
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplecomplete__input {
|
||||||
|
@media screen and (max-width: 992px) {
|
||||||
|
animation-name: highlightAnim;
|
||||||
|
animation-duration: 1.3s;
|
||||||
|
animation-timing-function: ease-in-out;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-busy {
|
||||||
|
border-color: rgba(255,255,255,.6);
|
||||||
|
background: $primary;
|
||||||
|
background: linear-gradient(90deg, lighten(red, 30) 0%, lighten(yellow, 30) 25%, lighten(green, 30) 50%, lighten(blue, 30) 75%, lighten(red, 30) 100%);
|
||||||
|
background-size: 10vw 10vw;
|
||||||
|
animation-name: scrollingBg;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-duration: 3s;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
|
||||||
|
.simplecomplete__input-field {
|
||||||
|
border-color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simplecomplete__results-list {
|
||||||
|
padding-top: 1em;
|
||||||
|
&-empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before, &:after {
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
border-radius: $border-radius;
|
||||||
|
height: 1.2em;
|
||||||
|
background: $light-gray;
|
||||||
|
width: 80%;
|
||||||
|
margin: 0 1em;
|
||||||
|
animation-name: pulsingAnim;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-duration: .8s;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
width: 60%;
|
||||||
|
margin-top: .6em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.has-results) {
|
||||||
|
.simplecomplete__results {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/simplecomplete/index.js
Normal file
7
src/simplecomplete/index.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import SimpleComplete from "./simplecomplete.js";
|
||||||
|
|
||||||
|
/// Make SimpleComplete a global variable.
|
||||||
|
window.SimpleComplete = SimpleComplete;
|
||||||
|
|
||||||
|
export default SimpleComplete;
|
||||||
|
export { SimpleComplete };
|
||||||
37
src/simplecomplete/options.js
Normal file
37
src/simplecomplete/options.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const defaultOptions = {
|
||||||
|
url: null,
|
||||||
|
method: 'GET',
|
||||||
|
headers: null,
|
||||||
|
delay: 300,
|
||||||
|
timeout: 30,
|
||||||
|
multiple: false,
|
||||||
|
paramName: 'q',
|
||||||
|
debug: false,
|
||||||
|
minLength: null,
|
||||||
|
placeholder: 'Search',
|
||||||
|
blankable: false,
|
||||||
|
|
||||||
|
// ui stuff and options/settings
|
||||||
|
params () {},
|
||||||
|
init () {},
|
||||||
|
error () {},
|
||||||
|
|
||||||
|
// user interaction
|
||||||
|
focus () {}, // user clicks input
|
||||||
|
blur () {}, // user leaves input
|
||||||
|
onInput () {}, // user starts input
|
||||||
|
keydown () {}, // user presses a key
|
||||||
|
keyup () {}, // user releases a key
|
||||||
|
select () {}, // user clicks an element
|
||||||
|
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
|
||||||
|
onRenderItem (out, data) { return out }, // renders an item from response
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultOptions
|
||||||
515
src/simplecomplete/simplecomplete.js
Normal file
515
src/simplecomplete/simplecomplete.js
Normal file
@@ -0,0 +1,515 @@
|
|||||||
|
import contentloaded from './utils/contentloaded.js'
|
||||||
|
import _defaults from './options.js'
|
||||||
|
|
||||||
|
export default class SimpleComplete {
|
||||||
|
static KEYS = {
|
||||||
|
ENTER: 13,
|
||||||
|
TAB: 9
|
||||||
|
}
|
||||||
|
static initClass () {
|
||||||
|
this.prototype.events = []
|
||||||
|
|
||||||
|
window.contentloaded = contentloaded
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (el, options = null) {
|
||||||
|
this.results = null
|
||||||
|
this.query = null
|
||||||
|
this.launchedRequest = null
|
||||||
|
this.options = {
|
||||||
|
..._defaults,
|
||||||
|
...(options || {})
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('string' === typeof el) {
|
||||||
|
let els = document.querySelectorAll(el)
|
||||||
|
if (els && els.length > 0) {
|
||||||
|
els.forEach(el => {
|
||||||
|
if (!SimpleComplete.instances) {
|
||||||
|
SimpleComplete.instances = []
|
||||||
|
}
|
||||||
|
SimpleComplete.instances.push(new SimpleComplete(el, options))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.element = el
|
||||||
|
|
||||||
|
if (this.element.simplecomplete) {
|
||||||
|
throw new Error('nope')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.element.simplecomplete = this
|
||||||
|
this.hideOrigin ()
|
||||||
|
this.initDOM ()
|
||||||
|
this.initEvents ()
|
||||||
|
|
||||||
|
this.updateLabel ()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideOrigin () {
|
||||||
|
if(!this.options.debug) {
|
||||||
|
this.element.style.width = '1px';
|
||||||
|
this.element.style.height = '1px';
|
||||||
|
this.element.style.position = 'absolute'
|
||||||
|
this.element.style.left = `${-window.innerWidth * 99}px`
|
||||||
|
this.element.style.top = '0'
|
||||||
|
} else {
|
||||||
|
this.element.style.border = 'solid 3px red'
|
||||||
|
this.element.style.marginTop = '1em'
|
||||||
|
this.element.style.marginBottom = '1em'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initDOM () {
|
||||||
|
this.parentNode = this.element.parentNode
|
||||||
|
|
||||||
|
this.wrapper = SimpleComplete.createElement('DIV', {
|
||||||
|
classes: 'simplecomplete'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.options.blankable) {
|
||||||
|
this.wrapper.classList.add('is-blankable')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isMultiple()) {
|
||||||
|
this.wrapper.classList.add('is-multiple')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isSelectElement()) {
|
||||||
|
this.wrapper.classList.add('is-select')
|
||||||
|
}
|
||||||
|
|
||||||
|
let inputWrapper = SimpleComplete.createElement('DIV', {
|
||||||
|
classes: 'simplecomplete__input'
|
||||||
|
})
|
||||||
|
|
||||||
|
this.fakeInput = SimpleComplete.createElement('DIV', {
|
||||||
|
classes: 'simplecomplete__input-field'
|
||||||
|
})
|
||||||
|
this.cancelInputBtn = SimpleComplete.createElement('A', {
|
||||||
|
classes: 'simplecomplete__input-cancel'
|
||||||
|
})
|
||||||
|
|
||||||
|
this.cancelInputBtn.addEventListener('click', e => {
|
||||||
|
e.preventDefault ()
|
||||||
|
e.stopPropagation ()
|
||||||
|
document.body.dispatchEvent(new Event('click'))
|
||||||
|
})
|
||||||
|
|
||||||
|
inputWrapper.appendChild(this.fakeInput)
|
||||||
|
inputWrapper.appendChild(this.cancelInputBtn)
|
||||||
|
|
||||||
|
this.currentValue = SimpleComplete.createElement('DIV', {
|
||||||
|
classes: 'simplecomplete__value'
|
||||||
|
})
|
||||||
|
|
||||||
|
this.wrapper.appendChild(this.currentValue)
|
||||||
|
this.wrapper.appendChild(inputWrapper)
|
||||||
|
|
||||||
|
// render results
|
||||||
|
let resultsWrapper = SimpleComplete.createElement('DIV', {
|
||||||
|
classes: 'simplecomplete__results'
|
||||||
|
})
|
||||||
|
|
||||||
|
this.resultsList = SimpleComplete.createElement('DIV', {
|
||||||
|
classes: 'simplecomplete__results-list'
|
||||||
|
})
|
||||||
|
|
||||||
|
resultsWrapper.appendChild(this.resultsList)
|
||||||
|
this.wrapper.appendChild(resultsWrapper)
|
||||||
|
|
||||||
|
// insert wrapper into parentnode of element
|
||||||
|
this.element.parentNode.insertBefore(this.wrapper, this.element.nextSibling)
|
||||||
|
|
||||||
|
this.onResults(Array.from(this.element.options || []))
|
||||||
|
}
|
||||||
|
|
||||||
|
initEvents () {
|
||||||
|
this.element.addEventListener('focus', e => {
|
||||||
|
e.preventDefault ()
|
||||||
|
this.onFocus ()
|
||||||
|
})
|
||||||
|
this.wrapper.addEventListener('click', this.onFocus.bind(this))
|
||||||
|
this.currentValue.addEventListener('click', this.onFocus.bind(this))
|
||||||
|
|
||||||
|
this.fakeInput.addEventListener('keydown', e => {
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case SimpleComplete.KEYS.TAB:
|
||||||
|
case SimpleComplete.KEYS.ENTER:
|
||||||
|
e.preventDefault()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.fakeInput.addEventListener('keyup', this.onInput.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
onInput (e) {
|
||||||
|
if (this.launchedRequest) {
|
||||||
|
window.clearTimeout(this.launchedRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case SimpleComplete.KEYS.ENTER:
|
||||||
|
this.onSend()
|
||||||
|
case SimpleComplete.KEYS.TAB:
|
||||||
|
e.preventDefault()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.query = e.target.textContent.trim()
|
||||||
|
this.launchedRequest = window.setTimeout(this.onSend.bind(this), this.options.delay || 300)
|
||||||
|
this.onResults()
|
||||||
|
}
|
||||||
|
|
||||||
|
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')
|
||||||
|
this.wrapper.classList.add('is-busy')
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.wrapper.classList.remove('is-busy')
|
||||||
|
if(this.queued) {
|
||||||
|
this.queued = false
|
||||||
|
this.onSend ()
|
||||||
|
}
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isMultiple () {
|
||||||
|
return this.element.multiple || false
|
||||||
|
}
|
||||||
|
|
||||||
|
deselectOption (_val) {
|
||||||
|
let _opts = Array.from(this.element.selectedOptions)
|
||||||
|
|
||||||
|
for (let _key in _opts) {
|
||||||
|
let o = _opts[_key]
|
||||||
|
if (o.value == _val) {
|
||||||
|
o.selected = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectOption (_val) {
|
||||||
|
let _opts = Array.from(this.element.options)
|
||||||
|
|
||||||
|
for (let _key in _opts) {
|
||||||
|
let o = _opts[_key]
|
||||||
|
if (o.value == _val) {
|
||||||
|
o.selected = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateValue (_val = null) {
|
||||||
|
if (_val !== null || this.options.blankable) {
|
||||||
|
if (this.isMultiple()) {
|
||||||
|
if (this.isSelected(_val)) {
|
||||||
|
this.deselectOption (_val)
|
||||||
|
} else {
|
||||||
|
this.selectOption (_val)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.element.value = _val
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
this.updateLabel ()
|
||||||
|
this.onResults ()
|
||||||
|
}
|
||||||
|
|
||||||
|
isSelectElement () {
|
||||||
|
return (this.element.tagName || false) && 'SELECT' == this.element.tagName
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLabel () {
|
||||||
|
this.currentValue.classList.remove('is-empty')
|
||||||
|
this.currentValue.innerHTML = ''
|
||||||
|
if (this.isMultiple() || this.isSelectElement()) {
|
||||||
|
let _opts = Array.from(this.element.selectedOptions)
|
||||||
|
for (let _key in _opts) {
|
||||||
|
let o = _opts[_key]
|
||||||
|
|
||||||
|
let badge = SimpleComplete.createElement('SPAN', {
|
||||||
|
classes: 'selected-option'
|
||||||
|
})
|
||||||
|
|
||||||
|
badge.textContent = o.textContent
|
||||||
|
badge.dataset.value = o.value
|
||||||
|
if (this.isMultiple() || this.options.blankable) {
|
||||||
|
badge.addEventListener('click', e => {
|
||||||
|
e.preventDefault ()
|
||||||
|
e.stopPropagation ()
|
||||||
|
this.deselectOption(e.target.dataset.value)
|
||||||
|
this.updateValue()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentValue.appendChild(badge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.currentValue.childNodes.length) {
|
||||||
|
let placeholder = this.element.value || this.element.getAttribute('placeholder') || this.element.dataset.placeholder || this.options.placeholder || 'Search'
|
||||||
|
if (this.isSelectElement()) {
|
||||||
|
let badge = SimpleComplete.createElement('SPAN', {
|
||||||
|
classes: 'empty-item'
|
||||||
|
})
|
||||||
|
|
||||||
|
badge.textContent = placeholder
|
||||||
|
|
||||||
|
this.currentValue.appendChild(badge)
|
||||||
|
} else {
|
||||||
|
this.currentValue.textContent = placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placeholder == this.options.placeholder) {
|
||||||
|
this.currentValue.classList.add('is-empty')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onError (key) {
|
||||||
|
console.log(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocus (e) {
|
||||||
|
if (e) {
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
document.body.dispatchEvent(new Event('click'))
|
||||||
|
this.wrapper.classList.add('active')
|
||||||
|
if (this.results.length > 0) {
|
||||||
|
this.wrapper.classList.add('has-results')
|
||||||
|
}
|
||||||
|
this.currentValue.classList.add('hide')
|
||||||
|
this.fakeInput.setAttribute('contenteditable', true)
|
||||||
|
this.fakeInput.focus()
|
||||||
|
|
||||||
|
this.options.focus()
|
||||||
|
|
||||||
|
document.body.addEventListener('click', e => {
|
||||||
|
this.onBlur()
|
||||||
|
}, {once:true})
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlur () {
|
||||||
|
this.query = null
|
||||||
|
this.fakeInput.textContent = ''
|
||||||
|
this.wrapper.classList.remove('active')
|
||||||
|
this.currentValue.classList.remove('hide')
|
||||||
|
this.fakeInput.setAttribute('contenteditable', false)
|
||||||
|
|
||||||
|
this.wrapper.classList.remove('has-results')
|
||||||
|
|
||||||
|
this.onResults ()
|
||||||
|
|
||||||
|
this.options.blur()
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelect (_val) {
|
||||||
|
this.updateValue (_val)
|
||||||
|
document.body.dispatchEvent(new Event('click'))
|
||||||
|
}
|
||||||
|
|
||||||
|
onResults (data = null) {
|
||||||
|
if(data !== null) {
|
||||||
|
this.results = data
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
} catch(err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.resultsList.childNodes.length) {
|
||||||
|
let emptyNode = SimpleComplete.createElement('DIV', {
|
||||||
|
classes: 'simplecomplete__results-list-empty'
|
||||||
|
})
|
||||||
|
|
||||||
|
emptyNode.textContent = this.options.emptyText || 'No results'
|
||||||
|
|
||||||
|
this.resultsList.appendChild(emptyNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isSelected (_val) {
|
||||||
|
let opts = Array.from(this.element.selectedOptions || [])
|
||||||
|
for (let _index = 0; _index < opts.length; _index++) {
|
||||||
|
let option = opts[_index]
|
||||||
|
if (option.value == _val) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.element.value == _val || false
|
||||||
|
}
|
||||||
|
|
||||||
|
renderOption (data) {
|
||||||
|
let _out = SimpleComplete.createElement('A', {
|
||||||
|
classes: 'simplecomplete__results-list-item'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.isSelected(data.value)) {
|
||||||
|
_out.classList.add('selected')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.disabled) {
|
||||||
|
_out.classList.add('disabled')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isSelected(data.value) || this.isMultiple()) {
|
||||||
|
_out.addEventListener('click', e => {
|
||||||
|
e.preventDefault ()
|
||||||
|
e.stopPropagation ()
|
||||||
|
if (!data.disabled) {
|
||||||
|
this.onSelect(data.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// cut the label and mark the search term in the label if the search term is set
|
||||||
|
if (!!this.query) {
|
||||||
|
let regexp = new RegExp(this.query,'gi')
|
||||||
|
let label = data.label
|
||||||
|
|
||||||
|
let check = label.matchAll(regexp)
|
||||||
|
|
||||||
|
if(check) {
|
||||||
|
let hightlightSpan = SimpleComplete.createElement('SPAN', {
|
||||||
|
classes: 'highlight'
|
||||||
|
});
|
||||||
|
let offset = 0;
|
||||||
|
[...check].forEach(res => {
|
||||||
|
hightlightSpan.textContent = res[0]
|
||||||
|
let parts = [
|
||||||
|
label.substr(0, res.index),
|
||||||
|
hightlightSpan.outerHTML,
|
||||||
|
label.substr(offset + res.index + res[0].length, label.length + offset)
|
||||||
|
]
|
||||||
|
|
||||||
|
offset += hightlightSpan.outerHTML.length
|
||||||
|
|
||||||
|
label = parts.join('')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_out.innerHTML = label
|
||||||
|
} else {
|
||||||
|
_out.textContent = data.label
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return this.options.onRenderItem(_out, data)
|
||||||
|
} catch(err) {
|
||||||
|
return _out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleComplete.initClass ()
|
||||||
|
|
||||||
|
SimpleComplete.version = 'dev'
|
||||||
|
|
||||||
|
SimpleComplete.options = {}
|
||||||
|
|
||||||
|
SimpleComplete.instances = []
|
||||||
|
|
||||||
|
SimpleComplete.autoDiscover = true
|
||||||
|
|
||||||
|
SimpleComplete.discover = () => {
|
||||||
|
let inputs = document.querySelectorAll('[data-sc]')
|
||||||
|
let res = []
|
||||||
|
|
||||||
|
if (inputs && inputs.length > 0) {
|
||||||
|
inputs.forEach(_in => {
|
||||||
|
console.log(_in, _in.dataset.blankable || false)
|
||||||
|
let options = {
|
||||||
|
minLength: _in.dataset.minLength || null,
|
||||||
|
blankable: _in.dataset.blankable || false
|
||||||
|
}
|
||||||
|
res.push(new SimpleComplete(_in, options));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleComplete.createElement = function (tag) {
|
||||||
|
let options = arguments[1] || {}
|
||||||
|
options = {
|
||||||
|
id: null,
|
||||||
|
classes: null,
|
||||||
|
attributes: null,
|
||||||
|
styles: null,
|
||||||
|
data: null,
|
||||||
|
...options
|
||||||
|
}
|
||||||
|
|
||||||
|
let el = document.createElement(tag)
|
||||||
|
|
||||||
|
if (el) {
|
||||||
|
if (options.styles) {
|
||||||
|
el.styles = options.styles
|
||||||
|
}
|
||||||
|
if (options.classes) {
|
||||||
|
if ('string' == typeof options.classes) {
|
||||||
|
options.classes = options.classes.split(' ')
|
||||||
|
}
|
||||||
|
options.classes.forEach(_class => el.classList.add(_class))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.attributes) {
|
||||||
|
Object.keys(options.attributes).forEach(_key => el.setAttribute(_key, options.attributes[_key]))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.data) {
|
||||||
|
Object.keys(options.data).forEach(_key => {
|
||||||
|
el.dataset[_key] = options.data[_key]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleComplete.getElement = (el, name) => {
|
||||||
|
let $el = null
|
||||||
|
|
||||||
|
if (document.querySelectorAll) {
|
||||||
|
$el = document.querySelector(el)
|
||||||
|
} else if(!!el.nodeType) {
|
||||||
|
$el = el
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$el) {
|
||||||
|
throw new Error('')
|
||||||
|
}
|
||||||
|
|
||||||
|
return $el
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleComplete.__autoDiscoverFunc = () => {
|
||||||
|
if (SimpleComplete.autoDiscover) {
|
||||||
|
return SimpleComplete.discover()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentloaded(window, SimpleComplete.__autoDiscoverFunc)
|
||||||
|
|
||||||
|
export { SimpleComplete }
|
||||||
56
src/simplecomplete/utils/contentloaded.js
Normal file
56
src/simplecomplete/utils/contentloaded.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* contentloaded.js
|
||||||
|
*
|
||||||
|
* Author: Diego Perini (diego.perini at gmail.com)
|
||||||
|
* Summary: cross-browser wrapper for DOMContentLoaded
|
||||||
|
* Updated: 20101020
|
||||||
|
* License: MIT
|
||||||
|
* Version: 1.2
|
||||||
|
*
|
||||||
|
* URL:
|
||||||
|
* http://javascript.nwbox.com/ContentLoaded/
|
||||||
|
* http://javascript.nwbox.com/ContentLoaded/MIT-LICENSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function contentLoaded (win, fn) {
|
||||||
|
let done = false;
|
||||||
|
let top = true;
|
||||||
|
let doc = win.document;
|
||||||
|
let root = doc.documentElement;
|
||||||
|
let add = doc.addEventListener ? "addEventListener" : "attachEvent";
|
||||||
|
let rem = doc.addEventListener ? "removeEventListener" : "detachEvent";
|
||||||
|
let pre = doc.addEventListener ? "" : "on";
|
||||||
|
var init = function (e) {
|
||||||
|
if (e.type === "readystatechange" && doc.readyState !== "complete") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(e.type === "load" ? win : doc)[rem](pre + e.type, init, false);
|
||||||
|
if (!done && (done = true)) {
|
||||||
|
return fn.call(win, e.type || e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var poll = function () {
|
||||||
|
try {
|
||||||
|
root.doScroll("left");
|
||||||
|
} catch (e) {
|
||||||
|
setTimeout(poll, 50);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return init("poll");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (doc.readyState !== "complete") {
|
||||||
|
if (doc.createEventObject && root.doScroll) {
|
||||||
|
try {
|
||||||
|
top = !win.frameElement;
|
||||||
|
} catch (error) {}
|
||||||
|
if (top) {
|
||||||
|
poll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
doc[add](pre + "DOMContentLoaded", init, false);
|
||||||
|
doc[add](pre + "readystatechange", init, false);
|
||||||
|
return win[add](pre + "load", init, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user