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