Run eslint on lib.js and fix problems that it spotted.
[quanweb.git] / app / lib.js
1 //QuanLib:  eBook Library
2 //(C) 2017 by Christian Jaekl (cejaekl@yahoo.com)
3
4 var g_state = {
5     cache: [],
6     count: 0,
7     mousePos: {    // Last known position of the mouse cursor
8         x: undefined,
9         y: undefined
10     },
11     first: 0,
12     ids: [],
13     last: (-1),
14     map: {},    // map from book.Id to index into cache[]
15     pageSize: 48,
16     tooltip: {
17         bookId: undefined,
18         milliSecs: 500,     // time to wait before displaying tooltip
19         mousePos: {
20             x: undefined,
21             y: undefined
22         },
23         threshold: 10,        // number of pixels that mouse can move before tip is dismissed
24         timer: undefined
25     }
26 };
27
28 document.onmousemove = onMouseMove;
29
30 function adjustPos(setting) {
31     var value = parseInt(setting);
32
33     if (g_state.first === value) {
34         // No change
35         return;
36     }
37
38     var maxFirst = Math.max(0, g_state.count - g_state.pageSize);
39
40     if (value < 0) {
41         g_state.first = 0;
42     } else if (value > maxFirst) {
43         g_state.first = maxFirst;
44     } else {
45         g_state.first = value;
46     }
47
48     g_state.last = g_state.first + g_state.pageSize - 1;
49     if (g_state.last >= g_state.count) {
50         g_state.last = g_state.count - 1;
51     }
52
53     document.getElementById('slider').value = setting;
54
55     refreshData();
56 }
57
58 function bookHtml(book) {
59     var result = '<div class="book">'
60         +   '<table>'
61         +     '<tr>'
62         +       '<td><a href="/book/' + book.Id + '">';
63     if (0 == book.CoverId) {
64         result +=          '(No cover available)';
65     } else {
66         result +=          '<img class="cover-thumb" src="/download/' + book.CoverId + '"/>';
67     }
68     result     +=       '</a></td>'
69         +       '<td onclick="displayDetails(' + book.Id + ');" '
70         +          ' onmouseover="startTooltipTimer(' + book.Id + ');">'
71         +         '<p><b>' + book.Title + '</b></p>'
72         +         '<p>'
73         +           '<i>' + book.AuthorReading + '</i>';
74     if (typeof(book.SeriesName) !== 'undefined' && book.SeriesName.length > 0) {
75         result +=          '<br/><i>' + book.SeriesName + ' ' + book.Volume + '</i>';
76     }
77     result     +=         '</p>'
78         +       '</td>'
79         +     '</tr>'
80         +   '</table>'
81         + '</div>';
82     return result;
83 }
84
85 //ce(s):  "clear if empty()"
86 //return s, unless it's undefined, in which case return an empty ("clear") string
87 function ce(s) {
88     if (typeof s !== 'undefined') {
89         return s;
90     }
91     return '';
92 }
93
94 function constructSearchUrl() {
95     var url = window.location.protocol + '//' + window.location.host + '/search/';
96
97     var firstTime = true;
98     var terms = ['aut', 'tit', 'ser'];
99
100     for (var idx in terms) {
101         var term = terms[idx];
102         var elem = document.getElementById(term);
103         if (null === elem) {
104             console.log('Error:  could not find form element for search term "' + term + '".');
105             continue;
106         }
107
108         var value = elem.value;
109         if (value.length > 0) {
110             if (firstTime) {
111                 url += '?';
112                 firstTime = false;
113             }
114             else {
115                 url += '&';
116             }
117             url += term + '=' + encodeURIComponent('%' + value + '%');
118         }
119     }
120
121     return url;
122 }
123
124 // Set the book ID for the details pane, and then show it
125 function displayDetails(bookId) {
126     g_state.tooltip.bookId = bookId;
127     showDetails();
128 }
129
130 function hideDetails() {
131     g_state.tooltip.mousePos.x = undefined;
132     g_state.tooltip.mousePos.y = undefined;
133     
134     var elem = document.getElementById('details');
135     elem.innerHTML = '';
136     elem.style.display = 'none';
137 }
138
139 function onMouseMove(event) {
140     if (typeof event === 'undefined') {
141         return;
142     }
143     
144     var x = event.pageX;
145     var y = event.pageY;
146     
147     if (  x === g_state.mousePos.x
148        && y === g_state.mousePos.y)
149     {
150         // No change from previous known position. 
151         // Nothing to see (or do) here, move along.
152         return;
153     }
154     
155     // Remember current mouse (x,y) position
156     g_state.mousePos.x = x;
157     g_state.mousePos.y = y;
158
159     // Is there an active tooltip?
160     if (typeof g_state.tooltip.mousePos.x === 'undefined') {
161         // No active tooltip, so nothing further to do
162         return;
163     }
164     
165     var deltaX = Math.abs(x - g_state.tooltip.mousePos.x);
166     var deltaY = Math.abs(y - g_state.tooltip.mousePos.y);
167     
168     if (  deltaX > g_state.tooltip.threshold
169        || deltaY > g_state.tooltip.threshold )
170     {
171         hideDetails();
172     }
173 }
174
175 function onNext() {
176     if (g_state.last < (g_state.count - 1)) {
177         adjustPos(g_state.first + g_state.pageSize);
178     }
179 }
180
181 function onPrev() {
182     if (g_state.first > 0) {
183         adjustPos(g_state.first - g_state.pageSize);
184     }
185 }
186
187 function onSlide(value) {
188     adjustPos(value);
189 }
190
191 function onSearch() {
192     var url = constructSearchUrl();
193
194     fetch(url, {method:'GET', cache:'default'})
195         .then(response => response.json())
196         .then((jsonValue) => {
197             // console.log('JSON response:  ', jsonValue);
198             g_state.ids = jsonValue;
199             g_state.count = g_state.ids.length;
200             g_state.first = (-1);
201     
202             var elem = document.getElementById('slider');
203             elem.max = g_state.count;
204     
205             adjustPos(0);
206         })
207         .catch(err => { 
208             var msg = 'Error fetching JSON from URL:  ' + url + ': ' + err + ':' + err.stack;
209             console.log(msg);
210             report(msg);
211         });
212 }
213
214 function refreshData() {
215     var i;
216     var url = '/info/?ids=';
217     g_state.map = {};
218     for (i = g_state.first; i <= g_state.last; ++i) {
219         if (i > g_state.first) {
220             url += ',';
221         }
222         var id = g_state.ids[i];
223         url += id;
224         g_state.map[id] = i - g_state.first;
225     }
226
227     fetch(url, {method:'GET', cache:'default'})
228         .then(response => response.json())
229         .then((jsonValue) => {
230             console.log('JSON response for info:  ', jsonValue);
231             g_state.cache = jsonValue;
232             refreshLayout();
233         })
234         .catch(err => {
235             var msg = 'Error fetching book details via URL:  ' + url + ': ' + err;
236             console.log(msg, err.stack);
237             report(msg);
238         });
239 }
240
241 function refreshLayout() {
242     var i;
243     var html = '';
244     var limit = g_state.last - g_state.first;
245     for (i = 0; i <= limit; ++i) {
246         var book = g_state.cache[i];
247         html += bookHtml(book);
248     }
249
250     document.getElementById('books').innerHTML = html;
251     document.getElementById('first').innerHTML = (g_state.first + 1);
252     document.getElementById('last').innerHTML = (g_state.last + 1);
253     document.getElementById('count').innerHTML = g_state.count;
254 }
255
256 function report(message) {
257     document.getElementById('books').innerHTML = message;
258 }
259
260 function showDetails() {
261     var id = g_state.tooltip.bookId;
262     var elem = document.getElementById('details');
263     var index = g_state.map[id];
264     var book = g_state.cache[index];
265     var html = '<div><p><b>' + book.Title + '</b></p>'
266     + '<p><i>' + ce(book.AuthorReading) + '<br/>' + ce(book.Series) + ' ' + ce(book.Volume) + '</i></p></div><div>'
267     + ce(book.Description)
268     + '</div>';
269
270     // Remember the current mouse (x,y).
271     // If we move the mouse too far from this point, that will trigger hiding the tooltip.
272     g_state.tooltip.mousePos.x = g_state.mousePos.x;
273     g_state.tooltip.mousePos.y = g_state.mousePos.y;
274
275     elem.innerHTML = html;
276
277     elem.style.display = 'block';    // show, and calculate size, so that we can query it below
278     
279     var x = g_state.mousePos.x;
280     var y = g_state.mousePos.y;
281     
282     var bcr = elem.getBoundingClientRect();
283     
284     var width = bcr.width;
285     var height = bcr.height;
286     
287     x = Math.max(x - (width / 2), 0);
288     y = Math.max(y - (height / 2), 0);
289     
290     elem.style.left = x + 'px';
291     elem.style.top = y + 'px';
292 }
293
294 function startTooltipTimer(bookId) {
295     if (typeof g_state.tooltip.timer !== 'undefined') {
296         clearTimeout(g_state.tooltip.timer);
297     }
298     g_state.tooltip.bookId = bookId;
299     g_state.tooltip.timer = setTimeout(showDetails, g_state.tooltip.milliSecs);
300 }
301
302 function stopTooltipTimer() {
303     if (typeof g_state.tooltip.timer === 'undefined') {
304         return;
305     }
306     
307     clearTimeout(g_state.tooltip.timer);
308     g_state.tooltip.timer = undefined;
309     hideDetails();
310 }
311