cbvirtua 1.0.24 → 1.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="autocomplete">
|
|
3
|
+
<div class="autocomplete__box" :class="{'autocomplete__searching' : showResults}">
|
|
4
|
+
|
|
5
|
+
<img v-if="!isLoading" class="autocomplete__icon" src="../assets/search.svg">
|
|
6
|
+
<img v-else class="autocomplete__icon animate-spin" src="../assets/loading.svg">
|
|
7
|
+
|
|
8
|
+
<div class="autocomplete__inputs">
|
|
9
|
+
<input
|
|
10
|
+
v-model="display"
|
|
11
|
+
:placeholder="placeholder"
|
|
12
|
+
:disabled="disableInput"
|
|
13
|
+
:maxlength="maxlength"
|
|
14
|
+
:class="inputClass"
|
|
15
|
+
@click="search"
|
|
16
|
+
@input="search"
|
|
17
|
+
@keydown.enter="enter"
|
|
18
|
+
@keydown.tab="close"
|
|
19
|
+
@keydown.up="up"
|
|
20
|
+
@keydown.down="down"
|
|
21
|
+
@keydown.esc="close"
|
|
22
|
+
@focus="focus"
|
|
23
|
+
@blur="blur"
|
|
24
|
+
type="text"
|
|
25
|
+
autocomplete="off">
|
|
26
|
+
<input :name="name" type="hidden" :value="value">
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<!-- clearButtonIcon -->
|
|
30
|
+
<span v-show="!disableInput && !isEmpty && !isLoading && !hasError" class="autocomplete__icon autocomplete--clear" @click="clear">
|
|
31
|
+
<span v-if="clearButtonIcon" :class="clearButtonIcon"></span>
|
|
32
|
+
<img v-else src="../assets/close.svg">
|
|
33
|
+
</span>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<ul v-show="showResults" class="autocomplete__results" :style="listStyle">
|
|
37
|
+
<slot name="results">
|
|
38
|
+
<!-- error -->
|
|
39
|
+
<li v-if="hasError" class="autocomplete__results__item autocomplete__results__item--error">{{ error }}</li>
|
|
40
|
+
|
|
41
|
+
<!-- results -->
|
|
42
|
+
<template v-if="!hasError">
|
|
43
|
+
<slot name="firstResult"></slot>
|
|
44
|
+
<li
|
|
45
|
+
v-for="(result, key) in results"
|
|
46
|
+
:key="key"
|
|
47
|
+
@click.prevent="select(result)"
|
|
48
|
+
class="autocomplete__results__item"
|
|
49
|
+
:class="{'autocomplete__selected' : isSelected(key) }"
|
|
50
|
+
v-html="formatDisplay(result)">
|
|
51
|
+
</li>
|
|
52
|
+
<slot name="lastResult"></slot>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<!-- no results -->
|
|
56
|
+
<li v-if="noResultMessage" class="autocomplete__results__item autocomplete__no-results">
|
|
57
|
+
<slot name="noResults">No Results.</slot>
|
|
58
|
+
</li>
|
|
59
|
+
</slot>
|
|
60
|
+
</ul>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<script type="text/babel">
|
|
65
|
+
import debounce from 'lodash/debounce'
|
|
66
|
+
export default {
|
|
67
|
+
props: {
|
|
68
|
+
/**
|
|
69
|
+
* Data source for the results
|
|
70
|
+
* `String` is a url, typed input will be appended
|
|
71
|
+
* `Function` received typed input, and must return a string; to be used as a url
|
|
72
|
+
* `Array` and `Object` (see `results-property`) are used directly
|
|
73
|
+
*/
|
|
74
|
+
source: {
|
|
75
|
+
type: [String, Function, Array, Object],
|
|
76
|
+
required: true
|
|
77
|
+
},
|
|
78
|
+
/**
|
|
79
|
+
* http method
|
|
80
|
+
*/
|
|
81
|
+
method: {
|
|
82
|
+
type: String,
|
|
83
|
+
default: 'get'
|
|
84
|
+
},
|
|
85
|
+
/**
|
|
86
|
+
* Input placeholder
|
|
87
|
+
*/
|
|
88
|
+
placeholder: {
|
|
89
|
+
default: 'Search'
|
|
90
|
+
},
|
|
91
|
+
/**
|
|
92
|
+
* Preset starting value
|
|
93
|
+
*/
|
|
94
|
+
initialValue: {
|
|
95
|
+
type: [String, Number]
|
|
96
|
+
},
|
|
97
|
+
/**
|
|
98
|
+
* Preset starting display value
|
|
99
|
+
*/
|
|
100
|
+
initialDisplay: {
|
|
101
|
+
type: String
|
|
102
|
+
},
|
|
103
|
+
/**
|
|
104
|
+
* CSS class for the surrounding input div
|
|
105
|
+
*/
|
|
106
|
+
inputClass: {
|
|
107
|
+
type: [String, Object]
|
|
108
|
+
},
|
|
109
|
+
/**
|
|
110
|
+
* To disable the input
|
|
111
|
+
*/
|
|
112
|
+
disableInput: {
|
|
113
|
+
type: Boolean
|
|
114
|
+
},
|
|
115
|
+
/**
|
|
116
|
+
* name property of the input holding the selected value
|
|
117
|
+
*/
|
|
118
|
+
name: {
|
|
119
|
+
type: String
|
|
120
|
+
},
|
|
121
|
+
/**
|
|
122
|
+
* api - property of results array
|
|
123
|
+
*/
|
|
124
|
+
resultsProperty: {
|
|
125
|
+
type: String
|
|
126
|
+
},
|
|
127
|
+
/**
|
|
128
|
+
* Results property used as the value
|
|
129
|
+
*/
|
|
130
|
+
resultsValue: {
|
|
131
|
+
type: String,
|
|
132
|
+
default: 'id'
|
|
133
|
+
},
|
|
134
|
+
/**
|
|
135
|
+
* Results property used as the display
|
|
136
|
+
*/
|
|
137
|
+
resultsDisplay: {
|
|
138
|
+
type: [String, Function],
|
|
139
|
+
default: 'name'
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Callback to format the server data
|
|
144
|
+
*/
|
|
145
|
+
resultsFormatter: {
|
|
146
|
+
type: Function
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Whether to show the no results message
|
|
151
|
+
*/
|
|
152
|
+
showNoResults: {
|
|
153
|
+
type: Boolean,
|
|
154
|
+
default: true
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Additional request headers
|
|
159
|
+
*/
|
|
160
|
+
requestHeaders: {
|
|
161
|
+
type: Object
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Credentials: same-origin, include, *omit
|
|
166
|
+
*/
|
|
167
|
+
credentials: {
|
|
168
|
+
type: String
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Optional clear button icon class
|
|
173
|
+
*/
|
|
174
|
+
clearButtonIcon: {
|
|
175
|
+
type: String
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Optional max input length
|
|
180
|
+
*/
|
|
181
|
+
maxlength: {
|
|
182
|
+
type: Number
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
data () {
|
|
186
|
+
return {
|
|
187
|
+
value: null,
|
|
188
|
+
display: null,
|
|
189
|
+
results: null,
|
|
190
|
+
selectedIndex: null,
|
|
191
|
+
loading: false,
|
|
192
|
+
isFocussed: false,
|
|
193
|
+
error: null,
|
|
194
|
+
selectedId: null,
|
|
195
|
+
selectedDisplay: null,
|
|
196
|
+
eventListener: false
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
computed: {
|
|
200
|
+
showResults () {
|
|
201
|
+
return Array.isArray(this.results) || this.hasError
|
|
202
|
+
},
|
|
203
|
+
noResults () {
|
|
204
|
+
return Array.isArray(this.results) && this.results.length === 0
|
|
205
|
+
},
|
|
206
|
+
noResultMessage () {
|
|
207
|
+
return this.noResults &&
|
|
208
|
+
!this.isLoading &&
|
|
209
|
+
this.isFocussed &&
|
|
210
|
+
!this.hasError &&
|
|
211
|
+
this.showNoResults
|
|
212
|
+
},
|
|
213
|
+
isEmpty () {
|
|
214
|
+
return !this.display
|
|
215
|
+
},
|
|
216
|
+
isLoading () {
|
|
217
|
+
return this.loading === true
|
|
218
|
+
},
|
|
219
|
+
hasError () {
|
|
220
|
+
return this.error !== null
|
|
221
|
+
},
|
|
222
|
+
listStyle () {
|
|
223
|
+
if (this.isLoading) {
|
|
224
|
+
return {
|
|
225
|
+
color: '#ccc'
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
methods: {
|
|
231
|
+
/**
|
|
232
|
+
* Search wrapper method
|
|
233
|
+
*/
|
|
234
|
+
search () {
|
|
235
|
+
this.selectedIndex = null
|
|
236
|
+
switch (true) {
|
|
237
|
+
case typeof this.source === 'string':
|
|
238
|
+
// No resource search with no input
|
|
239
|
+
if (!this.display || this.display.length < 1) {
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return this.resourceSearch(this.source + this.display)
|
|
244
|
+
case typeof this.source === 'function':
|
|
245
|
+
// No resource search with no input
|
|
246
|
+
if (!this.display || this.display.length < 1) {
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
return this.resourceSearch(this.source(this.display))
|
|
250
|
+
case Array.isArray(this.source):
|
|
251
|
+
return this.arrayLikeSearch()
|
|
252
|
+
default:
|
|
253
|
+
throw new TypeError()
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Debounce the typed search query before making http requests
|
|
259
|
+
* @param {String} url
|
|
260
|
+
*/
|
|
261
|
+
resourceSearch: debounce(function (url) {
|
|
262
|
+
if (!this.display) {
|
|
263
|
+
this.results = []
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
this.loading = true
|
|
267
|
+
this.setEventListener()
|
|
268
|
+
this.request(url)
|
|
269
|
+
}, 200),
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Make an http request for results
|
|
273
|
+
* @param {String} url
|
|
274
|
+
*/
|
|
275
|
+
request (url) {
|
|
276
|
+
let promise = fetch(url, {
|
|
277
|
+
method: this.method,
|
|
278
|
+
credentials: this.getCredentials(),
|
|
279
|
+
headers: this.getHeaders()
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
return promise
|
|
283
|
+
.then(response => {
|
|
284
|
+
if (response.ok) {
|
|
285
|
+
this.error = null
|
|
286
|
+
return response.json()
|
|
287
|
+
}
|
|
288
|
+
throw new Error('Network response was not ok.')
|
|
289
|
+
})
|
|
290
|
+
.then(response => {
|
|
291
|
+
this.results = this.setResults(response)
|
|
292
|
+
this.emitRequestResultEvent()
|
|
293
|
+
this.loading = false
|
|
294
|
+
})
|
|
295
|
+
.catch(error => {
|
|
296
|
+
this.error = error.message
|
|
297
|
+
this.loading = false
|
|
298
|
+
})
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Set some default headers and apply user supplied headers
|
|
303
|
+
*/
|
|
304
|
+
getHeaders () {
|
|
305
|
+
const headers = {
|
|
306
|
+
'Accept': 'application/json, text/plain, */*'
|
|
307
|
+
}
|
|
308
|
+
if (this.requestHeaders) {
|
|
309
|
+
for (var prop in this.requestHeaders) {
|
|
310
|
+
headers[prop] = this.requestHeaders[prop]
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return new Headers(headers)
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Set default credentials and apply user supplied value
|
|
318
|
+
*/
|
|
319
|
+
getCredentials () {
|
|
320
|
+
let credentials = 'same-origin'
|
|
321
|
+
if (this.credentials) {
|
|
322
|
+
credentials = this.credentials
|
|
323
|
+
}
|
|
324
|
+
return credentials
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Set results property from api response
|
|
329
|
+
* @param {Object|Array} response
|
|
330
|
+
* @return {Array}
|
|
331
|
+
*/
|
|
332
|
+
setResults (response) {
|
|
333
|
+
if (this.resultsFormatter) {
|
|
334
|
+
return this.resultsFormatter(response)
|
|
335
|
+
}
|
|
336
|
+
if (this.resultsProperty && response[this.resultsProperty]) {
|
|
337
|
+
return response[this.resultsProperty]
|
|
338
|
+
}
|
|
339
|
+
if (Array.isArray(response)) {
|
|
340
|
+
return response
|
|
341
|
+
}
|
|
342
|
+
return []
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Emit an event based on the request results
|
|
347
|
+
*/
|
|
348
|
+
emitRequestResultEvent () {
|
|
349
|
+
if (this.results.length === 0) {
|
|
350
|
+
this.$emit('noResults', {query: this.display})
|
|
351
|
+
} else {
|
|
352
|
+
this.$emit('results', {results: this.results})
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Search in results passed via an array
|
|
358
|
+
*/
|
|
359
|
+
arrayLikeSearch () {
|
|
360
|
+
this.setEventListener()
|
|
361
|
+
if (!this.display) {
|
|
362
|
+
this.results = this.source
|
|
363
|
+
this.$emit('results', {results: this.results})
|
|
364
|
+
this.loading = false
|
|
365
|
+
return true
|
|
366
|
+
}
|
|
367
|
+
this.results = this.source.filter((item) => {
|
|
368
|
+
return this.formatDisplay(item).toLowerCase().includes(this.display.toLowerCase())
|
|
369
|
+
})
|
|
370
|
+
this.$emit('results', {results: this.results})
|
|
371
|
+
this.loading = false
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Select a result
|
|
376
|
+
* @param {Object}
|
|
377
|
+
*/
|
|
378
|
+
select (obj) {
|
|
379
|
+
if (!obj) {
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
this.value = (this.resultsValue && obj[this.resultsValue]) ? obj[this.resultsValue] : obj.id
|
|
383
|
+
this.display = this.formatDisplay(obj)
|
|
384
|
+
this.selectedDisplay = this.display
|
|
385
|
+
this.$emit('selected', {
|
|
386
|
+
value: this.value,
|
|
387
|
+
display: this.display,
|
|
388
|
+
selectedObject: obj
|
|
389
|
+
})
|
|
390
|
+
this.$emit('input', this.value)
|
|
391
|
+
this.close()
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* @param {Object} obj
|
|
396
|
+
* @return {String}
|
|
397
|
+
*/
|
|
398
|
+
formatDisplay (obj) {
|
|
399
|
+
switch (typeof this.resultsDisplay) {
|
|
400
|
+
case 'function':
|
|
401
|
+
return this.resultsDisplay(obj)
|
|
402
|
+
case 'string':
|
|
403
|
+
if (!obj[this.resultsDisplay]) {
|
|
404
|
+
throw new Error(`"${this.resultsDisplay}" property expected on result but is not defined.`)
|
|
405
|
+
}
|
|
406
|
+
return obj[this.resultsDisplay]
|
|
407
|
+
default:
|
|
408
|
+
throw new TypeError()
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Register the component as focussed
|
|
414
|
+
*/
|
|
415
|
+
focus () {
|
|
416
|
+
this.isFocussed = true
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Remove the focussed value
|
|
421
|
+
*/
|
|
422
|
+
blur () {
|
|
423
|
+
this.isFocussed = false
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Is this item selected?
|
|
428
|
+
* @param {Object}
|
|
429
|
+
* @return {Boolean}
|
|
430
|
+
*/
|
|
431
|
+
isSelected (key) {
|
|
432
|
+
return key === this.selectedIndex
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Focus on the previous results item
|
|
437
|
+
*/
|
|
438
|
+
up () {
|
|
439
|
+
if (this.selectedIndex === null) {
|
|
440
|
+
this.selectedIndex = this.results.length - 1
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
this.selectedIndex = (this.selectedIndex === 0) ? this.results.length - 1 : this.selectedIndex - 1
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Focus on the next results item
|
|
448
|
+
*/
|
|
449
|
+
down () {
|
|
450
|
+
if (this.selectedIndex === null) {
|
|
451
|
+
this.selectedIndex = 0
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
this.selectedIndex = (this.selectedIndex === this.results.length - 1) ? 0 : this.selectedIndex + 1
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Select an item via the keyboard
|
|
459
|
+
*/
|
|
460
|
+
enter () {
|
|
461
|
+
if (this.selectedIndex === null) {
|
|
462
|
+
this.$emit('nothingSelected', this.display)
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
this.select(this.results[this.selectedIndex])
|
|
466
|
+
this.$emit('enter', this.display)
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Clear all values, results and errors
|
|
471
|
+
*/
|
|
472
|
+
clear () {
|
|
473
|
+
this.display = null
|
|
474
|
+
this.value = null
|
|
475
|
+
this.results = null
|
|
476
|
+
this.error = null
|
|
477
|
+
this.$emit('clear')
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Close the results list. If nothing was selected clear the search
|
|
482
|
+
*/
|
|
483
|
+
close () {
|
|
484
|
+
if (!this.value || !this.selectedDisplay) {
|
|
485
|
+
this.clear()
|
|
486
|
+
}
|
|
487
|
+
if (this.selectedDisplay !== this.display && this.value) {
|
|
488
|
+
this.display = this.selectedDisplay
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
this.results = null
|
|
492
|
+
this.error = null
|
|
493
|
+
this.removeEventListener()
|
|
494
|
+
this.$emit('close')
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Add event listener for clicks outside the results
|
|
499
|
+
*/
|
|
500
|
+
setEventListener () {
|
|
501
|
+
if (this.eventListener) {
|
|
502
|
+
return false
|
|
503
|
+
}
|
|
504
|
+
this.eventListener = true
|
|
505
|
+
document.addEventListener('click', this.clickOutsideListener, true)
|
|
506
|
+
return true
|
|
507
|
+
},
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Remove the click event listener
|
|
511
|
+
*/
|
|
512
|
+
removeEventListener () {
|
|
513
|
+
this.eventListener = false
|
|
514
|
+
document.removeEventListener('click', this.clickOutsideListener, true)
|
|
515
|
+
},
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Method invoked by the event listener
|
|
519
|
+
*/
|
|
520
|
+
clickOutsideListener (event) {
|
|
521
|
+
if (this.$el && !this.$el.contains(event.target)) {
|
|
522
|
+
this.close()
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
mounted () {
|
|
527
|
+
this.value = this.initialValue
|
|
528
|
+
this.display = this.initialDisplay
|
|
529
|
+
this.selectedDisplay = this.initialDisplay
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
</script>
|
|
533
|
+
|
|
534
|
+
<style lang="stylus">
|
|
535
|
+
.autocomplete
|
|
536
|
+
position relative
|
|
537
|
+
width 100%
|
|
538
|
+
*
|
|
539
|
+
box-sizing border-box
|
|
540
|
+
|
|
541
|
+
.autocomplete__box
|
|
542
|
+
display flex
|
|
543
|
+
align-items center
|
|
544
|
+
background #fff
|
|
545
|
+
border: 1px solid #ccc
|
|
546
|
+
border-radius 3px
|
|
547
|
+
padding 0 5px
|
|
548
|
+
|
|
549
|
+
.autocomplete__searching
|
|
550
|
+
border-radius 3px 3px 0 0
|
|
551
|
+
|
|
552
|
+
.autocomplete__inputs
|
|
553
|
+
flex-grow 1
|
|
554
|
+
padding 0 5px
|
|
555
|
+
input
|
|
556
|
+
width 100%
|
|
557
|
+
border 0
|
|
558
|
+
&:focus
|
|
559
|
+
outline none
|
|
560
|
+
|
|
561
|
+
.autocomplete--clear
|
|
562
|
+
cursor pointer
|
|
563
|
+
|
|
564
|
+
.autocomplete__results
|
|
565
|
+
margin 0
|
|
566
|
+
padding 0
|
|
567
|
+
list-style-type none
|
|
568
|
+
z-index 1000
|
|
569
|
+
position absolute
|
|
570
|
+
max-height 400px
|
|
571
|
+
overflow-y auto
|
|
572
|
+
background white
|
|
573
|
+
width 100%
|
|
574
|
+
border 1px solid #ccc
|
|
575
|
+
border-top 0
|
|
576
|
+
color black
|
|
577
|
+
|
|
578
|
+
.autocomplete__results__item--error
|
|
579
|
+
color red
|
|
580
|
+
|
|
581
|
+
.autocomplete__results__item
|
|
582
|
+
padding 7px 10px
|
|
583
|
+
cursor pointer
|
|
584
|
+
&:hover
|
|
585
|
+
background rgba(0, 180, 255, 0.075)
|
|
586
|
+
&.autocomplete__selected
|
|
587
|
+
background rgba(0, 180, 255, 0.15)
|
|
588
|
+
|
|
589
|
+
.autocomplete__icon
|
|
590
|
+
height 14px
|
|
591
|
+
width 14px
|
|
592
|
+
|
|
593
|
+
.animate-spin
|
|
594
|
+
animation spin 2s infinite linear
|
|
595
|
+
|
|
596
|
+
@keyframes spin
|
|
597
|
+
from
|
|
598
|
+
transform rotate(0deg)
|
|
599
|
+
to
|
|
600
|
+
transform rotate(360deg)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
</style>
|
package/package.json
CHANGED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<view class="rect submit_btn flexbox_auto" id="btn" style="top:{{top}}px;left:{{left}}px;" bindtap="onSubmit" bindtouchmove="onTouchmove" bindtouchstart="onTouchStart">
|
|
2
|
+
提交
|
|
3
|
+
</view>
|
|
4
|
+
———
|
|
5
|
+
let startX = 0 // 获取手指初始坐标
|
|
6
|
+
let startY = 0
|
|
7
|
+
let x = 0 // 获得盒子原来的位置
|
|
8
|
+
let y = 0
|
|
9
|
+
let cSys = {} // 当前系统配置
|
|
10
|
+
let btnsRect = [] // 拖拽元素宽高
|
|
11
|
+
let defaultH = 100 // 缺省高度
|
|
12
|
+
let defaultW = 100 // 缺省宽度
|
|
13
|
+
|
|
14
|
+
Component({
|
|
15
|
+
lifetimes: {
|
|
16
|
+
ready: function () {
|
|
17
|
+
const query = wx.createSelectorQuery().in(this)
|
|
18
|
+
query.select('#btn').boundingClientRect(res => {
|
|
19
|
+
cSys = wx.getSystemInfoSync()
|
|
20
|
+
btnsRect = [res.width, res.height]
|
|
21
|
+
const top = Math.floor(cSys.windowHeight - btnsRect[1] - defaultH)
|
|
22
|
+
const left = Math.floor(cSys.windowWidth - btnsRect[0] - defaultW)
|
|
23
|
+
this.setData({
|
|
24
|
+
top,
|
|
25
|
+
left
|
|
26
|
+
})
|
|
27
|
+
}).exec();
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
data: {
|
|
31
|
+
top: 0,
|
|
32
|
+
left: 0,
|
|
33
|
+
},
|
|
34
|
+
methods: {
|
|
35
|
+
onTouchStart (e) {
|
|
36
|
+
// 获取手指初始坐标
|
|
37
|
+
startX = e.changedTouches[0].pageX;
|
|
38
|
+
startY = e.changedTouches[0].pageY;
|
|
39
|
+
x = e.currentTarget.offsetLeft;
|
|
40
|
+
y = e.currentTarget.offsetTop;
|
|
41
|
+
},
|
|
42
|
+
onTouchmove (e) {
|
|
43
|
+
// 计算手指的移动距离:手指移动之后的坐标减去手指初始的坐标
|
|
44
|
+
const moveX = e.changedTouches[0].pageX - startX;
|
|
45
|
+
const moveY = e.changedTouches[0].pageY - startY;
|
|
46
|
+
// 移动盒子 盒子原来的位置 + 手指移动的距离
|
|
47
|
+
const top = Math.floor(Math.min(Math.max(0, y + moveY), cSys.windowHeight - btnsRect[1]))
|
|
48
|
+
const left = Math.floor(Math.min(Math.max(0, x + moveX), cSys.windowWidth - btnsRect[0]))
|
|
49
|
+
this.setData({
|
|
50
|
+
top,
|
|
51
|
+
left
|
|
52
|
+
})
|
|
53
|
+
},
|
|
54
|
+
onSubmit () {
|
|
55
|
+
console.warn('提交');
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
————————————————
|
|
60
|
+
page{
|
|
61
|
+
width: 100%;
|
|
62
|
+
height: 100%;
|
|
63
|
+
background-color: #F5F5F5;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.submit_btn{
|
|
67
|
+
background: rgba(0, 0, 0, .05);
|
|
68
|
+
border-radius: 10rpx;
|
|
69
|
+
border: 2rpx solid #999999;
|
|
70
|
+
color: #999999;
|
|
71
|
+
font-size: 28rpx;
|
|
72
|
+
position: absolute;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.flexbox_auto{
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
justify-content: center;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.rect{
|
|
82
|
+
height: 100rpx;
|
|
83
|
+
width: 120rpx;
|
|
84
|
+
}
|
|
85
|
+
————————————————
|