<body>
<form>
<input id="search" onclick="onSearch();" type="button" value="Search"/>
- Author: <input id="aut" type="text"/>
+ Author: <input id="aut" type="text"/>
Title: <input id="tit" type="text"/>
Series: <input id="ser" type="text"/>
</form>
<div id="details" class="tooltip" onclick="hideDetails();">(No information available)</div>
- <script src="lib.js"></script>
+ <script src="lib.min.js"></script>
</body>
</html>
--- /dev/null
+module.exports = {
+ "env": {
+ "browser": true,
+ "es6": true
+ },
+ "extends": "eslint:recommended",
+ "parserOptions": {
+ "sourceType": "module"
+ },
+ "rules": {
+ "indent": [
+ "error",
+ 4
+ ],
+ "linebreak-style": [
+ "error",
+ "unix"
+ ],
+ "no-console": "off",
+ "no-unused-vars": "off",
+ "quotes": [
+ "error",
+ "single"
+ ],
+ "semi": [
+ "error",
+ "always"
+ ]
+ }
+};
--- /dev/null
+// ==========
+// BooksModel
+
+var BooksModel = (function() {
+
+ var my = {};
+
+ // =================
+ // Private variables
+
+ var listeners = [];
+
+ // ================
+ // Public variables
+
+ my.cache = [];
+ my.count = 0,
+ my.first = 0,
+ my.ids = [],
+ my.last = (-1),
+ my.map = {}, // map from book.Id to index into cache[]
+ my.pageSize = 20;
+
+ // ==============
+ // Public methods
+
+ my.listen = function(subscriber) {
+ listeners.push(subscriber);
+ };
+
+ my.refreshData = function () {
+ var i;
+ var url = '/info/?ids=';
+ my.map = {};
+ for (i = my.first; i <= my.last; ++i) {
+ if (i > my.first) {
+ url += ',';
+ }
+ var id = my.ids[i];
+ url += id;
+ my.map[id] = i - my.first;
+ }
+
+ fetch(url, {method:'GET', cache:'default'})
+ .then(response => response.json())
+ .then((jsonValue) => {
+ console.log('JSON response for info: ', jsonValue);
+ my.cache = jsonValue;
+ notifyAll(); // inform all subscribers that the model has been updated
+ })
+ .catch(err => {
+ var msg = 'Error fetching book details via URL: ' + url + ': ' + err;
+ console.log(msg, err.stack);
+ report(msg);
+ });
+ };
+
+ // ===============
+ // Private methods
+
+ function notifyAll() {
+ for (var i in listeners) {
+ listeners[i].notify();
+ }
+ }
+
+ return my;
+})();
--- /dev/null
+// =========
+// BooksView
+
+var BooksView = (function() {
+ var my = {};
+
+ // ========================
+ // Private member variables
+ var booksModel = undefined;
+
+ // ==============
+ // Public Methods
+
+ my.init = function(linkedBooksModel) {
+ booksModel = linkedBooksModel;
+
+ booksModel.listen(my);
+ };
+
+ // Called when the model changes
+ my.notify = function () {
+ var i;
+ var html = '';
+ var limit = BooksModel.last - BooksModel.first;
+ for (i = 0; i <= limit; ++i) {
+ var book = BooksModel.cache[i];
+ html += bookHtml(book);
+ }
+
+ document.getElementById('books').innerHTML = html;
+ document.getElementById('first').innerHTML = (BooksModel.first + 1);
+ document.getElementById('last').innerHTML = (BooksModel.last + 1);
+ document.getElementById('count').innerHTML = BooksModel.count;
+ };
+
+ // ===============
+ // Private methods
+
+ 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="ToolTip.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;
+ }
+
+ return my;
+})();
--- /dev/null
+module.exports = function(grunt) {
+
+ // Project configuration.
+ grunt.initConfig({
+ pkg: grunt.file.readJSON('package.json'),
+ uglify: {
+ options: {
+ banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n',
+ mangle: false
+ },
+ app: {
+ files: {
+ '../app/lib.min.js': ['BooksModel.js', 'BooksView.js', 'PagingController.js', 'ToolTip.js', 'Main.js']
+ }
+ }
+ }
+ });
+
+ // Load the plugin that provides the "uglify" task.
+ grunt.loadNpmTasks('grunt-contrib-uglify');
+
+ // Default task(s).
+ grunt.registerTask('default', ['uglify']);
+
+};
--- /dev/null
+//QuanLib: eBook Library
+//(C) 2017 by Christian Jaekl (cejaekl@yahoo.com)
+
+'use strict';
+
+// Global state information (yuck). TODO: refactor this to compartmentalize.
+var g_state = {
+ mousePos: { // Last known position of the mouse cursor
+ x: undefined,
+ y: undefined
+ }
+};
+
+// ==============
+// Initialization
+
+document.onmousemove = onMouseMove;
+
+BooksView.init(BooksModel);
+PagingController.init(BooksModel);
+
+// ================
+// Global functions
+//
+// TODO: refactor this to compartmentalize more functionality.
+
+function report(message) {
+ document.getElementById('books').innerHTML = message;
+}
+
+function constructSearchUrl() {
+ 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 + '%');
+ }
+ }
+
+ return url;
+}
+
+function onMouseMove(event) {
+ if (typeof event === 'undefined') {
+ return;
+ }
+
+ 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;
+
+ ToolTip.mouseMoved(x, y);
+}
+
+function onNext() {
+ if (BooksModel.last < (BooksModel.count - 1)) {
+ PagingController.adjustPos(BooksModel.first + BooksModel.pageSize);
+ }
+}
+
+function onPrev() {
+ if (BooksModel.first > 0) {
+ PagingController.adjustPos(BooksModel.first - BooksModel.pageSize);
+ }
+}
+
+function onSlide(value) {
+ PagingController.adjustPos(value);
+}
+
+function onSearch() {
+ var url = constructSearchUrl();
+
+ fetch(url, {method:'GET', cache:'default'})
+ .then(response => response.json())
+ .then((jsonValue) => {
+ // console.log('JSON response: ', jsonValue);
+ BooksModel.ids = jsonValue;
+ BooksModel.count = BooksModel.ids.length;
+ BooksModel.first = (-1);
+
+ var elem = document.getElementById('slider');
+ elem.max = BooksModel.count;
+
+ PagingController.adjustPos(0);
+ })
+ .catch(err => {
+ var msg = 'Error fetching JSON from URL: ' + url + ': ' + err + ':' + err.stack;
+ console.log(msg);
+ report(msg);
+ });
+}
+
--- /dev/null
+// ================
+// PagingController
+
+var PagingController = (function() {
+ var my = {};
+
+ var booksModel = undefined;
+
+ // ==============
+ // Public Methods
+
+ my.init = function(linkedBooksModel) {
+ booksModel = linkedBooksModel;
+ };
+
+ my.adjustPos = function (setting) {
+ var value = parseInt(setting);
+
+ if (booksModel.first === value) {
+ // No change
+ return;
+ }
+
+ var maxFirst = Math.max(0, booksModel.count - booksModel.pageSize);
+
+ if (value < 0) {
+ booksModel.first = 0;
+ } else if (value > maxFirst) {
+ booksModel.first = maxFirst;
+ } else {
+ booksModel.first = value;
+ }
+
+ booksModel.last = booksModel.first + booksModel.pageSize - 1;
+ if (booksModel.last >= booksModel.count) {
+ booksModel.last = booksModel.count - 1;
+ }
+
+ document.getElementById('slider').value = setting;
+
+ booksModel.refreshData();
+ };
+
+ return my;
+})();
--- /dev/null
+// =======
+// ToolTip
+
+var ToolTip = (function () {
+ // =================
+ // Private variables
+ var my = {},
+ 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;
+
+ // ================
+ // Public variables
+ my.booksModel = undefined;
+
+ // ==============
+ // Public Methods
+
+ // Set the book ID for the details pane, and then show it
+ my.displayDetails = function (newBookId) {
+ bookId = newBookId;
+ my.showDetails();
+ };
+
+ // Hide the details pane, if it is currently visible
+ my.hideDetails = function () {
+ mousePos.x = undefined;
+ mousePos.y = undefined;
+
+ var elem = document.getElementById('details');
+ elem.innerHTML = '';
+ elem.style.display = 'none';
+ };
+
+ my.mouseMoved = function (x, y) {
+ // Is there an active tooltip?
+ if (typeof mousePos.x === 'undefined') {
+ // No active tooltip, so nothing further to do
+ return;
+ }
+
+ var deltaX = Math.abs(x - mousePos.x);
+ var deltaY = Math.abs(y - mousePos.y);
+
+ if ( deltaX > threshold
+ || deltaY > threshold )
+ {
+ my.hideDetails();
+ }
+ };
+
+ // Show the details pane
+ my.showDetails = function () {
+ var id = bookId;
+ var elem = document.getElementById('details');
+ var index = BooksModel.map[id];
+ var book = BooksModel.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.
+ mousePos.x = g_state.mousePos.x;
+ 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 = mousePos.x;
+ var y = 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';
+ };
+
+ my.startTooltipTimer = function (newBookId) {
+ if (typeof timer !== 'undefined') {
+ clearTimeout(timer);
+ }
+ bookId = newBookId;
+ timer = setTimeout(my.showDetails, milliSecs);
+ };
+
+ my.stopTooltipTimer = function () {
+ if (typeof timer === 'undefined') {
+ return;
+ }
+
+ clearTimeout(timer);
+ timer = undefined;
+ my.hideDetails();
+ };
+
+ // ===============
+ // Private methods
+
+ // 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 '';
+ }
+
+ return my;
+})();
--- /dev/null
+{
+ "name": "quanweb",
+ "version": "0.0.1",
+ "license": "GPL-3.0+",
+ "devDependencies": {
+ "grunt": "^1.0.1",
+ "grunt-contrib-jshint": "~0.10.0",
+ "grunt-contrib-nodeunit": "~0.4.1",
+ "grunt-contrib-uglify": "git://github.com/gruntjs/grunt-contrib-uglify.git#harmony"
+ }
+}