diff options
-rw-r--r-- | .babelrc | 6 | ||||
-rw-r--r-- | assets_src/js/lib/ajax.js | 60 | ||||
-rw-r--r-- | assets_src/js/lib/dom.js | 14 | ||||
-rw-r--r-- | assets_src/js/lib/search.js | 77 | ||||
-rw-r--r-- | assets_src/js/list.js | 42 | ||||
-rw-r--r-- | package.json.tpl | 3 | ||||
-rw-r--r-- | views/includes/instant_search_results.html | 4 | ||||
-rw-r--r-- | views/pages/list.html | 5 |
8 files changed, 157 insertions, 54 deletions
@@ -1,3 +1,7 @@ | |||
1 | { | 1 | { |
2 | "presets": ["es2015"] | 2 | "presets": ["es2015"], |
3 | "plugins": [ | ||
4 | "syntax-async-functions", | ||
5 | "transform-regenerator" | ||
6 | ] | ||
3 | } | 7 | } |
diff --git a/assets_src/js/lib/ajax.js b/assets_src/js/lib/ajax.js new file mode 100644 index 0000000..048f516 --- /dev/null +++ b/assets_src/js/lib/ajax.js | |||
@@ -0,0 +1,60 @@ | |||
1 | export class AjaxError extends Error { | ||
2 | constructor(status, statusText) { | ||
3 | let message = `${status}: ${statusText}` | ||
4 | super(message); | ||
5 | this.name = this.constructor.name; | ||
6 | this.message = message; | ||
7 | this.status = status; | ||
8 | this.statusText = statusText; | ||
9 | Error.captureStackTrace(this, this.constructor.name) | ||
10 | } | ||
11 | } | ||
12 | |||
13 | function applyOptions(xhr, options) { | ||
14 | options.headers = options.headers || []; | ||
15 | for (let header in options.headers) { | ||
16 | xhr.setRequestHeader(header, options.headers[header]); | ||
17 | } | ||
18 | } | ||
19 | |||
20 | function createHandler(xhr, success, fail) { | ||
21 | return () => { | ||
22 | if (xhr.readyState !== XMLHttpRequest.DONE) { | ||
23 | return; | ||
24 | } | ||
25 | if (xhr.status === 200) { | ||
26 | success(xhr.response); | ||
27 | } else { | ||
28 | fail(xhr.status, xhr.statusText); | ||
29 | } | ||
30 | }; | ||
31 | } | ||
32 | |||
33 | function request(method, url, body, options) { | ||
34 | options = options || {}; | ||
35 | return new Promise((resolve, reject) => { | ||
36 | let xhr = new XMLHttpRequest(); | ||
37 | xhr.onreadystatechange = createHandler( | ||
38 | xhr, | ||
39 | (res) => { | ||
40 | resolve(res); | ||
41 | }, | ||
42 | (status, statusText) => { | ||
43 | reject(new AjaxError(status, statusText)); | ||
44 | }); | ||
45 | xhr.open(method, url, true); | ||
46 | applyOptions(xhr, options); | ||
47 | xhr.onerror = function() { | ||
48 | reject(new AjaxError(-1, 'Network error')); | ||
49 | }; | ||
50 | xhr.send(body); | ||
51 | }); | ||
52 | } | ||
53 | |||
54 | export async function get(url, options) { | ||
55 | return request('GET', url, null, options); | ||
56 | } | ||
57 | |||
58 | export async function post(url, body, options) { | ||
59 | return request('POST', url, body, options); | ||
60 | } | ||
diff --git a/assets_src/js/lib/dom.js b/assets_src/js/lib/dom.js new file mode 100644 index 0000000..33845af --- /dev/null +++ b/assets_src/js/lib/dom.js | |||
@@ -0,0 +1,14 @@ | |||
1 | export function ready(fn) { | ||
2 | document.addEventListener('DOMContentLoaded', (e) => { | ||
3 | fn(); | ||
4 | }, false); | ||
5 | } | ||
6 | |||
7 | export function closest(el, fn) { | ||
8 | while (el) { | ||
9 | if (fn(el)) { | ||
10 | return el; | ||
11 | } | ||
12 | el = el.parentNode | ||
13 | } | ||
14 | } | ||
diff --git a/assets_src/js/lib/search.js b/assets_src/js/lib/search.js index 0cb33df..85559ef 100644 --- a/assets_src/js/lib/search.js +++ b/assets_src/js/lib/search.js | |||
@@ -1,40 +1,51 @@ | |||
1 | var strokeTimeout = null; | 1 | import * as dom from './dom'; |
2 | import * as ajax from './ajax'; | ||
2 | 3 | ||
3 | function instantSearch() { | 4 | export class InstantSearch { |
4 | if (strokeTimeout) { | 5 | constructor(element, resultsElem) { |
5 | clearTimeout(strokeTimeout); | 6 | this.resultClicked = () => {}; |
6 | } | 7 | this.element = element; |
7 | strokeTimeout = setTimeout(doSearch, 150); | 8 | this.resultsElem = resultsElem; |
8 | } | 9 | this.strokeTimeout = null; |
9 | 10 | element.addEventListener('keydown', e => { | |
10 | function doSearch() { | 11 | if (this.strokeTimeout) { |
11 | value = document.getElementById("search").value; | 12 | clearTimeout(this.strokeTimeout); |
12 | |||
13 | if (value === "") { | ||
14 | strokeTimeout = null; | ||
15 | document.getElementById("search-results").innerHTML = ""; | ||
16 | return; | ||
17 | } | ||
18 | var xhr = new XMLHttpRequest(); | ||
19 | xhr.onreadystatechange = function() { | ||
20 | if (xhr.readyState == XMLHttpRequest.DONE) { | ||
21 | if (xhr.status === 404 || xhr.status === 400 || typeof xhr.response === 'undefined' || xhr.response === '404 not found') { | ||
22 | document.getElementById("search-results").innerHTML = ''; | ||
23 | return | ||
24 | } | 13 | } |
25 | if (value === "") { | 14 | this.strokeTimeout = setTimeout(() => this.doSearch(), 150); |
26 | document.getElementById("search-results").innerHTML = ""; | 15 | }, false); |
27 | return; | 16 | resultsElem.addEventListener('click', e => { |
17 | let li = dom.closest(e.target, el => el.tagName.match(/li/i)); | ||
18 | if (!li) { | ||
19 | return true; | ||
28 | } | 20 | } |
21 | this.resultsElem.innerHTML = ''; | ||
22 | this.resultClicked(li.getAttribute('data-id')); | ||
23 | }, true); | ||
24 | } | ||
29 | 25 | ||
30 | document.getElementById("search-results").innerHTML = xhr.response.replace(new RegExp('{ (.*?)(' + value + ')(.*?) }', 'gi'), "$1<b>$2</b>$3").replace(new RegExp('{ (.*?) }', 'gi'), '$1'); | 26 | async doSearch() { |
27 | let value = this.element.value; | ||
28 | if (value.length < 2) { | ||
29 | this.strokeTimeout = null; | ||
30 | this.resultsElem.innerHTML = ""; | ||
31 | return; | ||
32 | } | ||
33 | let response = null; | ||
34 | try { | ||
35 | response = await ajax.get(`/search/gril_instant/${value}`); | ||
36 | } catch (e) { | ||
37 | this.resultsElem.innerHTML = ''; | ||
38 | return; | ||
31 | } | 39 | } |
40 | this.resultsElem.innerHTML = response | ||
41 | .replace( | ||
42 | new RegExp('{ (.*?)(' + | ||
43 | value + | ||
44 | ')(.*?) }', 'gi'), | ||
45 | "$1<b>$2</b>$3") | ||
46 | .replace( | ||
47 | new RegExp('{ (.*?) }', 'gi'), | ||
48 | '$1'); | ||
49 | this.strokeTimeout = null; | ||
32 | } | 50 | } |
33 | xhr.open('GET', '/search/gril_instant/' + value, true); | ||
34 | xhr.send(null); | ||
35 | strokeTimeout = null; | ||
36 | } | 51 | } |
37 | |||
38 | function clickSearchResult(resId) { | ||
39 | |||
40 | } \ No newline at end of file | ||
diff --git a/assets_src/js/list.js b/assets_src/js/list.js index 25ff80d..413f517 100644 --- a/assets_src/js/list.js +++ b/assets_src/js/list.js | |||
@@ -1,17 +1,29 @@ | |||
1 | function clickSearchResult(resId) { | 1 | import 'babel-polyfill'; |
2 | var xhr = new XMLHttpRequest(); | 2 | import * as search from './lib/search'; |
3 | xhr.onreadystatechange = function() { | 3 | import * as dom from './lib/dom'; |
4 | if (xhr.readyState == XMLHttpRequest.DONE) { | 4 | import * as ajax from './lib/ajax'; |
5 | if (xhr.status !== 200) { | ||
6 | alert(xhr.status + ": " + xhr.response); | ||
7 | return; | ||
8 | } | ||
9 | 5 | ||
10 | data = xhr.responseText; | 6 | async function resultClicked(grilId) { |
11 | document.getElementById("gril-list").innerHTML += data; | 7 | let response = null; |
12 | } | 8 | try { |
9 | response = await ajax.post( | ||
10 | window.location, | ||
11 | 'id=' + grilId, | ||
12 | { headers: { | ||
13 | 'Content-type': 'application/x-www-form-urlencoded' | ||
14 | } }); | ||
15 | } catch (e) { | ||
16 | alert(e.message); | ||
17 | return; | ||
13 | } | 18 | } |
14 | xhr.open('POST', window.location, true); | 19 | document.getElementById('gril-list').innerHTML += response; |
15 | xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); | 20 | } |
16 | xhr.send('id=' + resId); | 21 | |
17 | } \ No newline at end of file | 22 | function main() { |
23 | let is = new search.InstantSearch( | ||
24 | document.getElementById('gril-add-search'), | ||
25 | document.getElementById('search-results')); | ||
26 | is.resultClicked = resultClicked; | ||
27 | } | ||
28 | |||
29 | dom.ready(main); | ||
diff --git a/package.json.tpl b/package.json.tpl index c2d075b..8d0bb1a 100644 --- a/package.json.tpl +++ b/package.json.tpl | |||
@@ -3,7 +3,10 @@ | |||
3 | "version": "%($VERSION%)", | 3 | "version": "%($VERSION%)", |
4 | "devDependencies": { | 4 | "devDependencies": { |
5 | "less": "2.5.3", | 5 | "less": "2.5.3", |
6 | "babel-polyfill": "6.3.14", | ||
6 | "babel-preset-es2015": "6.3.13", | 7 | "babel-preset-es2015": "6.3.13", |
8 | "babel-plugin-syntax-async-functions": "6.3.13", | ||
9 | "babel-plugin-transform-regenerator": "6.4.4", | ||
7 | "browserify": "13.0.0", | 10 | "browserify": "13.0.0", |
8 | "babelify": "7.2.0" | 11 | "babelify": "7.2.0" |
9 | } | 12 | } |
diff --git a/views/includes/instant_search_results.html b/views/includes/instant_search_results.html index 0dd445c..acdbb30 100644 --- a/views/includes/instant_search_results.html +++ b/views/includes/instant_search_results.html | |||
@@ -1,8 +1,8 @@ | |||
1 | {{ define "instant_search_results" }} | 1 | {{ define "instant_search_results" }} |
2 | {{ range .results }} | 2 | {{ range .results }} |
3 | <li class="collection-item search-result avatar hoverable valign-wrapper" onClick="clickSearchResult({{ .ID }})" > | 3 | <li class="collection-item search-result avatar hoverable valign-wrapper" data-id="{{ .ID }}"> |
4 | <img src="/{{ .ImagePath }}" alt="" class="circle"> | 4 | <img src="/{{ .ImagePath }}" alt="" class="circle"> |
5 | <span class="title valign">{ {{ .Name }} }</span> | 5 | <span class="title valign">{ {{ .Name }} }</span> |
6 | </li> | 6 | </li> |
7 | {{ end }} | 7 | {{ end }} |
8 | {{ end }} \ No newline at end of file | 8 | {{ end }} |
diff --git a/views/pages/list.html b/views/pages/list.html index c85e147..c608b77 100644 --- a/views/pages/list.html +++ b/views/pages/list.html | |||
@@ -7,7 +7,6 @@ | |||
7 | <title>grilist</title> | 7 | <title>grilist</title> |
8 | <link rel="stylesheet" href="/assets/css/list.css" /> | 8 | <link rel="stylesheet" href="/assets/css/list.css" /> |
9 | <link rel="stylesheet" href="/assets/css/search.css" /> | 9 | <link rel="stylesheet" href="/assets/css/search.css" /> |
10 | <script src="/assets/js/search.js"></script> | ||
11 | <script src="/assets/js/list.js"></script> | 10 | <script src="/assets/js/list.js"></script> |
12 | <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> | 11 | <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> |
13 | </head> | 12 | </head> |
@@ -35,8 +34,8 @@ | |||
35 | <span style="font-size: 130%">Gril hinzufügen</span><br /> | 34 | <span style="font-size: 130%">Gril hinzufügen</span><br /> |
36 | <form> | 35 | <form> |
37 | <div class="input-field"> | 36 | <div class="input-field"> |
38 | <input id="search" type="search" onKeyDown="instantSearch()" required> | 37 | <input id="gril-add-search" class="gril-search" type="search" required> |
39 | <label for="search"><i class="material-icons">search</i></label> | 38 | <label for="gril-add-search"><i class="material-icons">search</i></label> |
40 | <i class="material-icons">close</i> | 39 | <i class="material-icons">close</i> |
41 | </div> | 40 | </div> |
42 | </form> | 41 | </form> |