Refactor JS into modules, and add basic MVC.
[quanweb.git] / app / lib.js
index 7175471c084b4822b8f479e84b91858c78d079e8..4a77ffc6e7950bdbe68fc5a1876aee71bafd296a 100644 (file)
-// QuanLib:  eBook Library
-// (C) 2017 by Christian Jaekl (cejaekl@yahoo.com)
+//QuanLib:  eBook Library
+//(C) 2017 by Christian Jaekl (cejaekl@yahoo.com)
 
-g_state = {
-  cache: {},
-  count: 0,
-  first: 0,
-  ids: [],
-  last: 0,
+'use strict';
+
+var g_state = {
+    cache: [],
+    count: 0,
+    mousePos: {    // Last known position of the mouse cursor
+        x: undefined,
+        y: undefined
+    },
+    first: 0,
+    ids: [],
+    last: (-1),
+    map: {},    // map from book.Id to index into cache[]
+    pageSize: 20,
+    tooltip: {
+        bookId: undefined,
+        milliSecs: 500,     // time to wait before displaying tooltip
+        mousePos: {
+            x: undefined,
+            y: undefined
+        },
+        threshold: 10,        // number of pixels that mouse can move before tip is dismissed
+        timer: undefined
+    }
+};
+
+document.onmousemove = onMouseMove;
+
+function adjustPos(setting) {
+    var value = parseInt(setting);
+
+    if (g_state.first === value) {
+        // No change
+        return;
+    }
+
+    var maxFirst = Math.max(0, g_state.count - g_state.pageSize);
+
+    if (value < 0) {
+        g_state.first = 0;
+    } else if (value > maxFirst) {
+        g_state.first = maxFirst;
+    } else {
+        g_state.first = value;
+    }
+
+    g_state.last = g_state.first + g_state.pageSize - 1;
+    if (g_state.last >= g_state.count) {
+        g_state.last = g_state.count - 1;
+    }
+
+    document.getElementById('slider').value = setting;
+
+    refreshData();
+}
+
+function bookHtml(book) {
+    var result = '<div class="book">'
+        +   '<table>'
+        +     '<tr>'
+        +       '<td><a href="/book/' + book.Id + '">';
+    if (0 == book.CoverId) {
+        result +=          '(No cover available)';
+    } else {
+        result +=          '<img class="cover-thumb" src="/download/' + book.CoverId + '"/>';
+    }
+    result     +=       '</a></td>'
+        +       '<td onclick="displayDetails(' + book.Id + ');" '
+        +          ' onmouseover="startTooltipTimer(' + book.Id + ');">'
+        +         '<p><b>' + book.Title + '</b></p>'
+        +         '<p>'
+        +           '<i>' + book.AuthorReading + '</i>';
+    if (typeof(book.SeriesName) !== 'undefined' && book.SeriesName.length > 0) {
+        result +=          '<br/><i>' + book.SeriesName + ' ' + book.Volume + '</i>';
+    }
+    result     +=         '</p>'
+        +       '</td>'
+        +     '</tr>'
+        +   '</table>'
+        + '</div>';
+    return result;
+}
+
+//ce(s):  "clear if empty()"
+//return s, unless it's undefined, in which case return an empty ("clear") string
+function ce(s) {
+    if (typeof s !== 'undefined') {
+        return s;
+    }
+    return '';
 }
 
 function constructSearchUrl() {
-  var url = window.location.protocol + '//' + window.location.host + '/search/';
+    var url = window.location.protocol + '//' + window.location.host + '/search/';
+
+    var firstTime = true;
+    var terms = ['aut', 'tit', 'ser'];
+
+    for (var idx in terms) {
+        var term = terms[idx];
+        var elem = document.getElementById(term);
+        if (null === elem) {
+            console.log('Error:  could not find form element for search term "' + term + '".');
+            continue;
+        }
+
+        var value = elem.value;
+        if (value.length > 0) {
+            if (firstTime) {
+                url += '?';
+                firstTime = false;
+            }
+            else {
+                url += '&';
+            }
+            url += term + '=' + encodeURIComponent('%' + value + '%');
+        }
+    }
 
-  var firstTime = true;
-  var terms = ['aut', 'tit', 'ser'];
+    return url;
+}
+
+// Set the book ID for the details pane, and then show it
+function displayDetails(bookId) {
+    g_state.tooltip.bookId = bookId;
+    showDetails();
+}
+
+function hideDetails() {
+    g_state.tooltip.mousePos.x = undefined;
+    g_state.tooltip.mousePos.y = undefined;
+    
+    var elem = document.getElementById('details');
+    elem.innerHTML = '';
+    elem.style.display = 'none';
+}
 
-  for (idx in terms) {
-    var term = terms[idx];
-    var elem = document.getElementById(term);
-    if (null === elem) {
-      console.log('Error:  could not find form element for search term "' + term + '".');
-      continue;
+function onMouseMove(event) {
+    if (typeof event === 'undefined') {
+        return;
     }
     
-    var value = elem.value;
-    if (value.length > 0) {
-      if (firstTime) {
-        url += '?';
-        firstTime = false;
-      }
-      else {
-        url += '&';
-      }
-      url += term + '=' + encodeURIComponent('%' + value + '%');
+    var x = event.pageX;
+    var y = event.pageY;
+    
+    if (  x === g_state.mousePos.x
+       && y === g_state.mousePos.y)
+    {
+        // No change from previous known position. 
+        // Nothing to see (or do) here, move along.
+        return;
     }
-  }
+    
+    // Remember current mouse (x,y) position
+    g_state.mousePos.x = x;
+    g_state.mousePos.y = y;
 
-  return url;
+    // Is there an active tooltip?
+    if (typeof g_state.tooltip.mousePos.x === 'undefined') {
+        // No active tooltip, so nothing further to do
+        return;
+    }
+    
+    var deltaX = Math.abs(x - g_state.tooltip.mousePos.x);
+    var deltaY = Math.abs(y - g_state.tooltip.mousePos.y);
+    
+    if (  deltaX > g_state.tooltip.threshold
+       || deltaY > g_state.tooltip.threshold )
+    {
+        hideDetails();
+    }
 }
 
 function onNext() {
+    if (g_state.last < (g_state.count - 1)) {
+        adjustPos(g_state.first + g_state.pageSize);
+    }
 }
 
 function onPrev() {
+    if (g_state.first > 0) {
+        adjustPos(g_state.first - g_state.pageSize);
+    }
 }
 
-function onSearch() {
-  console.log('onSearch()');
-
-  var url = constructSearchUrl();
+function onSlide(value) {
+    adjustPos(value);
+}
 
-  report('Loading data from server, please wait...')
-  console.log('Fetching:  "' + url + '"...')
+function onSearch() {
+    var url = constructSearchUrl();
 
-  fetch(url, {method:'GET', cache:'default'})
-  .then(response => response.json())
-  .then((jsonValue) => {
-    console.log('JSON response:  ', jsonValue);
-    g_state.ids = jsonValue
-    g_state.first = 0
-    g_state.last = (g_state.ids.length) - 1;
-    if (g_state.last > 100) {
-      g_state.last = 100;
-    }
-    refreshData()
-  })
-  .catch(err => { 
-    var msg = 'Error fetching JSON from URL:  ' + url + ': ' + err + ':' + err.stack;
-    console.log(msg);
-    report(msg);
-  });
+    fetch(url, {method:'GET', cache:'default'})
+        .then(response => response.json())
+        .then((jsonValue) => {
+            // console.log('JSON response:  ', jsonValue);
+            g_state.ids = jsonValue;
+            g_state.count = g_state.ids.length;
+            g_state.first = (-1);
+    
+            var elem = document.getElementById('slider');
+            elem.max = g_state.count;
+    
+            adjustPos(0);
+        })
+        .catch(err => { 
+            var msg = 'Error fetching JSON from URL:  ' + url + ': ' + err + ':' + err.stack;
+            console.log(msg);
+            report(msg);
+        });
 }
 
 function refreshData() {
-  report('Loading details for books ' + g_state.first + ' through ' + g_state.last + ', please wait...');
-
-  var i;
-  var url = '/info/?ids=';
-  for (i = g_state.first; i <= g_state.last; ++i) {
-    if (i > 0) {
-      url += ',';
-    }
-    url += g_state.ids[i];
-  }
-
-  fetch(url, {method:'GET', cache:'default'})
-  .then(response => response.json())
-  .then((jsonValue) => {
-    console.log('JSON response for info:  ', jsonValue);
-    report('');
-    g_state.cache = jsonValue;
-    refreshLayout();
-  })
-  .catch(err => {
-    var msg = 'Error fetching book details via URL:  ' + url + ': ' + err;
-    console.log(msg, err.stack);
-    report(msg);
-  });
+    var i;
+    var url = '/info/?ids=';
+    g_state.map = {};
+    for (i = g_state.first; i <= g_state.last; ++i) {
+        if (i > g_state.first) {
+            url += ',';
+        }
+        var id = g_state.ids[i];
+        url += id;
+        g_state.map[id] = i - g_state.first;
+    }
+
+    fetch(url, {method:'GET', cache:'default'})
+        .then(response => response.json())
+        .then((jsonValue) => {
+            console.log('JSON response for info:  ', jsonValue);
+            g_state.cache = jsonValue;
+            refreshLayout();
+        })
+        .catch(err => {
+            var msg = 'Error fetching book details via URL:  ' + url + ': ' + err;
+            console.log(msg, err.stack);
+            report(msg);
+        });
 }
 
 function refreshLayout() {
-  var i;
-  var html = '';
-  for (i = g_state.first; i <= g_state.last; ++i) {
-    var book = g_state.cache[i];
-    html += bookHtml(book);
-  }
+    var i;
+    var html = '';
+    var limit = g_state.last - g_state.first;
+    for (i = 0; i <= limit; ++i) {
+        var book = g_state.cache[i];
+        html += bookHtml(book);
+    }
 
-  document.getElementById('books').innerHTML = html;
+    document.getElementById('books').innerHTML = html;
+    document.getElementById('first').innerHTML = (g_state.first + 1);
+    document.getElementById('last').innerHTML = (g_state.last + 1);
+    document.getElementById('count').innerHTML = g_state.count;
 }
 
 function report(message) {
-  document.getElementById('books').innerHTML = message;
+    document.getElementById('books').innerHTML = message;
 }
 
-function bookHtml(book) {
-  console.log('bookHtml(): ', book);
-  var result = '<div class="book"><table><tr><td><a href="/book/' + book.Id + '">'
-             + '<img class="cover-thumb" src="/download/' + book.CoverId + '"/></a></td>'
-             + '<td><span class="popup">' + book.Description + '</span></td>'
-             + '</tr></table></div>';
-  return result;
+function showDetails() {
+    var id = g_state.tooltip.bookId;
+    var elem = document.getElementById('details');
+    var index = g_state.map[id];
+    var book = g_state.cache[index];
+    var html = '<div><p><b>' + book.Title + '</b></p>'
+    + '<p><i>' + ce(book.AuthorReading) + '<br/>' + ce(book.Series) + ' ' + ce(book.Volume) + '</i></p></div><div>'
+    + ce(book.Description)
+    + '</div>';
+
+    // Remember the current mouse (x,y).
+    // If we move the mouse too far from this point, that will trigger hiding the tooltip.
+    g_state.tooltip.mousePos.x = g_state.mousePos.x;
+    g_state.tooltip.mousePos.y = g_state.mousePos.y;
+
+    elem.innerHTML = html;
+
+    elem.style.display = 'block';    // show, and calculate size, so that we can query it below
+    
+    var x = g_state.mousePos.x;
+    var y = g_state.mousePos.y;
+    
+    var bcr = elem.getBoundingClientRect();
+    
+    var width = bcr.width;
+    var height = bcr.height;
+    
+    x = Math.max(x - (width / 2), 0);
+    y = Math.max(y - (height / 2), 0);
+    
+    elem.style.left = x + 'px';
+    elem.style.top = y + 'px';
+}
+
+function startTooltipTimer(bookId) {
+    if (typeof g_state.tooltip.timer !== 'undefined') {
+        clearTimeout(g_state.tooltip.timer);
+    }
+    g_state.tooltip.bookId = bookId;
+    g_state.tooltip.timer = setTimeout(showDetails, g_state.tooltip.milliSecs);
+}
+
+function stopTooltipTimer() {
+    if (typeof g_state.tooltip.timer === 'undefined') {
+        return;
+    }
+    
+    clearTimeout(g_state.tooltip.timer);
+    g_state.tooltip.timer = undefined;
+    hideDetails();
 }