+++ /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.adjustPos = function(setting) {
-
- var value = parseInt(setting);
-
- var prev = {
- first: my.first,
- last: my.last,
- };
-
- var maxFirst = Math.max(0, my.count - my.pageSize);
-
- if (value < 0) {
- my.first = 0;
- } else if (value > maxFirst) {
- my.first = maxFirst;
- } else {
- my.first = value;
- }
-
- my.last = my.first + my.pageSize - 1;
- if (my.last >= my.count) {
- my.last = my.count - 1;
- }
-
- if (prev.first !== my.first || prev.last !== my.last) {
- my.refreshData();
- }
- };
-
- 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(function(response) {return response.json();})
- .then(function(jsonValue) {
- my.cache = jsonValue;
- notifyAll(); // inform all subscribers that the model has been updated
- })
- .catch(function(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 + ');"'
- + ' onmouseleave="ToolTip.stopTooltipTimer();">'
- + '<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;
-})();
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', 'SearchController.js', 'ToolTip.js', 'Main.js']
- }
- }
- }
- });
+ // Project configuration.
+ grunt.initConfig({
+ concat: {
+ all: {
+ src: ['src/**/*.js', 'Main.js'],
+ dest: '../app/all.js',
+ nonull: true
+ },
+ tests: {
+ src: ['src/**/*.js', 'test/**/*.js'],
+ dest: 'all_tests.js',
+ nonull: true
+ },
+ },
+
+ pkg: grunt.file.readJSON('package.json'),
- // Load the plugin that provides the "uglify" task.
- grunt.loadNpmTasks('grunt-contrib-uglify');
+ uglify: {
+ options: {
+ banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n',
+ mangle: false
+ },
+ app: {
+ files: {
+ '../app/lib.min.js': ['src/**/*.js', 'Main.js']
+ }
+ }
+ }
+ });
- // Default task(s).
- grunt.registerTask('default', ['uglify']);
+ grunt.loadNpmTasks('grunt-contrib-concat');
+ grunt.loadNpmTasks('grunt-contrib-uglify');
+ // Default task(s).
+ grunt.registerTask('default', ['concat', 'uglify']);
};
// ==============
// Initialization
-document.onmousemove = onMouseMove;
+Browser.setOnMouseMove(onMouseMove);
BooksView.init(BooksModel);
PagingController.init(BooksModel);
// TODO: refactor this to compartmentalize more functionality.
function report(message) {
- document.getElementById('books').innerHTML = message;
+ Browser.getElementById('books').innerHTML = message;
}
function onMouseMove(event) {
+++ /dev/null
-// ================
-// PagingController
-
-var PagingController = (function() {
- var my = {};
-
- var booksModel = undefined;
-
- // ==============
- // Public Methods
-
- my.init = function(linkedBooksModel) {
- booksModel = linkedBooksModel;
- booksModel.listen(my);
- };
-
- my.adjustPos = function(setting) {
- var value = parseInt(setting);
-
- if (booksModel.first === value) {
- // No change
- return;
- }
-
- booksModel.adjustPos(setting);
- };
-
- my.notify = function() {
- document.getElementById('slider').value = booksModel.first;
- };
-
- return my;
-})();
--- /dev/null
+QuanWeb
+Web service to provide accces to a QuanLib database of e-books
+
+On the off chance that you want this code, you're welcome to it, subject to the
+GNU General Public Licence version 3 (or, at your option, any later version).
+
+Chris Jaekl
+cejaekl@yahoo.com
+++ /dev/null
-// ================
-// SearchController
-
-var SearchController = (function () {
- var my = {},
- booksModel = undefined;
-
- const terms = ['aut', 'tit', 'ser'];
-
- // ==============
- // Public methods
-
- my.init = function(linkedBooksModel) {
- booksModel = linkedBooksModel;
-
- for (var idx in terms) {
- addEnterListener(terms[idx]);
- }
- };
-
- my.onSearch = function() {
- var url = constructSearchUrl();
-
- fetch(url, {method:'GET', cache:'default'})
- .then(function(response) {return response.json();})
- .then(function(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(function(err) {
- var msg = 'Error fetching JSON from URL: ' + url + ': ' + err + ':' + err.stack;
- console.log(msg);
- report(msg);
- });
- };
-
- // ===============
- // Private methods
-
- // KeyUp listener. If the key is [Enter], then trigger a click on the [Search] button.
- function addEnterListener(ctrlId) {
- document.getElementById(ctrlId).addEventListener('keyup', function(event) {
- event.preventDefault();
- if (event.keyCode === 13) {
- document.getElementById('search').click();
- }
- });
- }
-
- function constructSearchUrl() {
- var url = window.location.protocol + '//' + window.location.host + '/search/';
-
- var firstTime = true;
-
- 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;
- }
-
- 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.stopTooltipTimer();
- 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;
- };
-
- // ===============
- // 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
+// Karma configuration
+// Generated on Sun Dec 31 2017 15:08:47 GMT+0900 (JST)
+
+module.exports = function(config) {
+ config.set({
+
+ // base path that will be used to resolve all patterns (eg. files, exclude)
+ basePath: '',
+
+
+ // frameworks to use
+ // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+ frameworks: ['jasmine'],
+
+
+ // list of files / patterns to load in the browser
+ files: [
+ 'all_tests.js'
+ ],
+
+
+ // list of files / patterns to exclude
+ exclude: [
+ ],
+
+
+ // preprocess matching files before serving them to the browser
+ // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+ preprocessors: {
+ },
+
+
+ // test results reporter to use
+ // possible values: 'dots', 'progress'
+ // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+ reporters: ['progress'],
+
+
+ // web server port
+ port: 9876,
+
+
+ // enable / disable colors in the output (reporters and logs)
+ colors: true,
+
+
+ // level of logging
+ // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+ logLevel: config.LOG_INFO,
+
+
+ // enable / disable watching file and executing tests whenever any file changes
+ autoWatch: true,
+
+
+ // start these browsers
+ // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+ browsers: ['Chrome'],
+
+
+ // Continuous Integration mode
+ // if true, Karma captures browsers, runs the tests and exits
+ singleRun: false,
+
+ // Concurrency level
+ // how many browser should be started simultaneous
+ concurrency: Infinity
+ })
+}
{
"name": "quanweb",
"version": "0.0.1",
+ "description": "E-Book Library Web User Interface",
"license": "GPL-3.0+",
"devDependencies": {
"grunt": "^1.0.1",
+ "grunt-contrib-concat": "^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"
+ "grunt-contrib-uglify": "git://github.com/gruntjs/grunt-contrib-uglify.git#harmony",
+ "grunt-mocha-test": "^0.13.3",
+ "jasmine": "^2.8.0",
+ "jasmine-core": "^2.8.0",
+ "karma": "^2.0.0",
+ "karma-chrome-launcher": "^2.2.0",
+ "karma-jasmine": "^1.1.1",
+ "mocha": "^4.0.1",
+ },
+ "scripts": {
+ "test": "./node_modules/karma/bin/karma start karma.conf.js"
}
}
--- /dev/null
+// ==========
+// BooksModel
+
+var BooksModel = (function() {
+
+ var my = {};
+
+ // =================
+ // Private variables
+
+ var listeners = [];
+
+ // ================
+ // Public variables
+
+ my.cache = [];
+ my.count = 0, // number of books available to be paged through
+ my.first = 0, // first book to be displayed in current page (0-indexed)
+ my.ids = [],
+ my.last = (-1), // last book to be displayed in the current page (0-indexed)
+ my.map = {}, // map from book.Id to index into cache[]
+ my.pageSize = 20;
+
+ // ==============
+ // Public methods
+
+ my.adjustPos = function(setting) {
+
+ var value = parseInt(setting);
+
+ var prev = {
+ first: my.first,
+ last: my.last,
+ };
+
+ var maxFirst = Math.max(0, my.count - my.pageSize);
+
+ if (value < 0) {
+ my.first = 0;
+ } else if (value > maxFirst) {
+ my.first = maxFirst;
+ } else {
+ my.first = value;
+ }
+
+ my.last = my.first + my.pageSize - 1;
+ if (my.last >= my.count) {
+ my.last = my.count - 1;
+ }
+
+ if (prev.first !== my.first || prev.last !== my.last) {
+ my.refreshData();
+ }
+ };
+
+ 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(function(response) {return response.json();})
+ .then(function(jsonValue) {
+ my.cache = jsonValue;
+ notifyAll(); // inform all subscribers that the model has been updated
+ })
+ .catch(function(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);
+ }
+
+ Browser.getElementById('books').innerHTML = html;
+ Browser.getElementById('first').innerHTML = (BooksModel.first + 1);
+ Browser.getElementById('last').innerHTML = (BooksModel.last + 1);
+ Browser.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 + ');"'
+ + ' onmouseleave="ToolTip.stopTooltipTimer();">'
+ + '<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
+// ================
+// PagingController
+
+var PagingController = (function() {
+ var my = {};
+
+ var booksModel = undefined;
+
+ // ==============
+ // Public Methods
+
+ my.init = function(linkedBooksModel) {
+ booksModel = linkedBooksModel;
+ booksModel.listen(my);
+ };
+
+ my.adjustPos = function(setting) {
+ var value = parseInt(setting);
+
+ if (booksModel.first === value) {
+ // No change
+ return;
+ }
+
+ booksModel.adjustPos(setting);
+ };
+
+ my.notify = function() {
+ Browser.getElementById('slider').value = booksModel.first;
+ };
+
+ return my;
+})();
--- /dev/null
+// ================
+// SearchController
+
+var SearchController = (function () {
+ var my = {},
+ booksModel = undefined;
+
+ const terms = ['aut', 'tit', 'ser'];
+
+ // ==============
+ // Public methods
+
+ my.init = function(linkedBooksModel) {
+ booksModel = linkedBooksModel;
+
+ for (var idx in terms) {
+ addEnterListener(terms[idx]);
+ }
+ };
+
+ my.onSearch = function() {
+ var url = constructSearchUrl();
+
+ fetch(url, {method:'GET', cache:'default'})
+ .then(function(response) {return response.json();})
+ .then(function(jsonValue) {
+ // console.log('JSON response: ', jsonValue);
+ booksModel.ids = jsonValue;
+ booksModel.count = booksModel.ids.length;
+ booksModel.first = (-1);
+
+ var elem = Browser.getElementById('slider');
+ elem.max = booksModel.count;
+
+ PagingController.adjustPos(0);
+ })
+ .catch(function(err) {
+ var msg = 'Error fetching JSON from URL: ' + url + ': ' + err + ':' + err.stack;
+ console.log(msg);
+ report(msg);
+ });
+ };
+
+ // ===============
+ // Private methods
+
+ // KeyUp listener. If the key is [Enter], then trigger a click on the [Search] button.
+ function addEnterListener(ctrlId) {
+ Browser.getElementById(ctrlId).addEventListener('keyup', function(event) {
+ event.preventDefault();
+ if (event.keyCode === 13) {
+ Browser.getElementById('search').click();
+ }
+ });
+ }
+
+ function constructSearchUrl() {
+ var url = window.location.protocol + '//' + window.location.host + '/search/';
+
+ var firstTime = true;
+
+ for (var idx in terms) {
+ var term = terms[idx];
+ var elem = Browser.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;
+ }
+
+ 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 = Browser.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.stopTooltipTimer();
+ my.hideDetails();
+ }
+ };
+
+ // Show the details pane
+ my.showDetails = function () {
+ var id = bookId;
+ var elem = Browser.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;
+ };
+
+ // ===============
+ // 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
+'use strict';
+
+describe('Scroll forward should not advance beyond end', function() {
+ var bm = BooksModel;
+ var calledRefreshData;
+
+ // [first, last, pageSize, count, setting, expectFirst, expectLast, expectRefresh]
+ data = [ [ 0, 19 , 20, 100, 20, 20 , 39 , true ],
+ [ 0, 19 , 20, 100, 100, 80 , 99 , true ],
+ [ 0, 19 , 20, 100, 80, 80 , 99 , true ],
+ [ 0, 19 , 20, 100, 79, 79 , 98 , true ],
+ [ 79, 99 , 20, 100, 50, 50 , 69 , true ],
+ [ 0, 11 , 20, 12, 19, 0 , 11 , false ],
+ [ 0, (-1), 20, 0, 20, 0 , (-1), false ]
+ ];
+
+ function doTest(datum) {
+ bm.first = datum[0];
+ bm.last = datum[1];
+ bm.pageSize = datum[2];
+ bm.count = datum[3];
+
+ var oldFunc = bm.refreshData;
+ calledRefreshData = false;
+ try {
+ bm.refreshData = function() {
+ calledRefreshData = true;
+ };
+
+ bm.adjustPos(datum[4]);
+ }
+ finally {
+ bm.refreshData = oldFunc;
+ }
+ }
+
+ it('should stay within valid range, and call refreshData() if bm.first changes', function() {
+ var i = 4;
+ for (i = 0; i < data.length; i += 1) {
+ datum = data[i];
+ doTest(datum);
+ expect(bm.first).toBe(datum[5]);
+ expect(bm.last).toBe(datum[6]);
+ expect(calledRefreshData).toBe(datum[7]);
+ }
+ });
+});
+
+