Koha/koha-tmpl/intranet-tmpl/lib/jquery/plugins/jquery.indexeddb.js
Jared Camins-Esakov 44d0ad451f Bug 10240: Offline circulation using HTML5 and IndexedDB
This patch adds an HTML5-based offline mode to Koha's existing
circulation module, allowing librarians to check out items using a
basically familiar interface. The feature will be implemented using
the Application Cache and IndexedDB features of the HTML5 specification,
both of which are fully supported on Firefox 10+ and Chrome 23+, with
limited support going back to Firefox 4 and Chrome 11. The basic
workflow enabled by this patch is as follows:

Part 1: While connected to the Internet
1. Enable offline functionality by turning on the
   "AllowOfflineCirculation" system preference.
2. Sync the offline circulation database on the computer that will be
   used for offline circulation by following the "Offline circulation
   interface" link on the Circulation home page, choosing "Synchronize (must be online)",
   and clicking the "Download records" button. This process may take a while.
3. Bookmark /cgi-bin/koha/circ/offline.pl (the page you are currently
   on) for easy access when offline.

Part 2: While disconnected from the Internet
4. Navigate to /cgi-bin/koha/circ/offline.pl using the bookmark you
   created while online.
5. Start checking books in by scanning the barcode of an item that has
   been returned into the box in the "Check in" tab.
6. Scan the barcodes of any additional items that have been returned.
7. Start checking out books to a patron by scanning the patron's barcode
   in the box in the "Check out" tab.
8. Set a due date (the "Remember for session" box will be checked by
   default, since circulation rules are not computed during offline
   transactions and therefore a due date must be specified by the
   librarian).
9. Scan an item barcode (if you did not set a due date, it will prompt
   you) to check the item out to the patron.
10. If a patron has a fine you can see the total amount (current to when
    the offline module was synced), and record a payment. Unlike when in
    online mode, there will be no breakdown of what item(s) fines are
    for, and you will only be able to record the payment amount and not
    associate it with a particular item.

Part 3: While connected to the Internet
11. Click the "Synchronize" link and choose "Upload transactions" to
    upload the transactions recorded during the offline circulation
    session.
12. Navigate to /cgi-bin/koha/offline_circ/list.pl (there will be a
    link from the Offline circulation page) and review the
    transactions, as described in the documentation for the Firefox
    Offline circulation plugin:
    http://wiki.koha-community.org/wiki/Offline_circulation_firefox_plugin

RM note: the IndexedDB jQuery plugin bundled with this patch is
copyright 2012 by Parashuram Narasimhan and other contributors and is
licensed under the MIT license.  The home page for the plugin is
http://nparashuram.com/jquery-indexeddb/.

Signed-off-by: Chris Cormack <chris@bigballofwax.co.nz>
Signed-off-by: Bernardo Gonzalez Kriegel <bgkriegel@gmail.com>

Comment: Works very well, no koha-qa errors

Test with Firefox 24.0
1) did some checkouts pre sync
2) synchronize database (Download)
3) go offline
4) Proceed to checkin some items from patron
5) Proceed to checkout items to patrons, setting date
6) Proceed to checkout to expired patron, warning appears
7) go online
8) Upload records
9) go to review transacctions and proceed
10) verified on patrons that checkin/out are done

Signed-off-by: Chris Cormack <chris@bigballofwax.co.nz>
Signed-off-by: Jonathan Druart <jonathan.druart@biblibre.com>
Signed-off-by: Galen Charlton <gmc@esilibrary.com>
2013-10-11 01:53:34 +00:00

517 lines
No EOL
15 KiB
JavaScript

(function($, undefined) {
var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;
var IDBCursor = window.IDBCursor || window.webkitIDBCursor;
IDBCursor.PREV = IDBCursor.PREV || "prev";
IDBCursor.NEXT = IDBCursor.NEXT || "next";
/**
* Best to use the constant IDBTransaction since older version support numeric types while the latest spec
* supports strings
*/
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
function getDefaultTransaction(mode) {
var result = null;
switch (mode) {
case 0:
case 1:
case "readwrite":
case "readonly":
result = mode;
break;
default:
result = IDBTransaction.READ_WRITE || "readwrite";
}
return result;
}
$.extend({
/**
* The IndexedDB object used to open databases
* @param {Object} dbName - name of the database
* @param {Object} config - version, onupgradeneeded, onversionchange, schema
*/
"indexedDB": function(dbName, config) {
if (config) {
// Parse the config argument
if (typeof config === "number") config = {
"version": config
};
var version = config.version;
if (config.schema && !version) {
var max = -1;
for (key in config.schema) {
max = max > key ? max : key;
}
version = config.version || max;
}
}
var wrap = {
"request": function(req, args) {
return $.Deferred(function(dfd) {
try {
var idbRequest = typeof req === "function" ? req(args) : req;
idbRequest.onsuccess = function(e) {
dfd.resolveWith(idbRequest, [idbRequest.result, e]);
};
idbRequest.onerror = function(e) {
dfd.rejectWith(idbRequest, [idbRequest.error, e]);
};
if (typeof idbRequest.onblocked !== "undefined" && idbRequest.onblocked === null) {
idbRequest.onblocked = function(e) {
var res;
try {
res = idbRequest.result;
} catch (e) {
res = null; // Required for Older Chrome versions, accessing result causes error
}
dfd.notifyWith(idbRequest, [res, e]);
};
}
if (typeof idbRequest.onupgradeneeded !== "undefined" && idbRequest.onupgradeneeded === null) {
idbRequest.onupgradeneeded = function(e) {
dfd.notifyWith(idbRequest, [idbRequest.result, e]);
};
}
} catch (e) {
e.name = "exception";
dfd.rejectWith(idbRequest, ["exception", e]);
}
});
},
// Wraps the IDBTransaction to return promises, and other dependent methods
"transaction": function(idbTransaction) {
return {
"objectStore": function(storeName) {
try {
return wrap.objectStore(idbTransaction.objectStore(storeName));
} catch (e) {
idbTransaction.readyState !== idbTransaction.DONE && idbTransaction.abort();
return wrap.objectStore(null);
}
},
"createObjectStore": function(storeName, storeParams) {
try {
return wrap.objectStore(idbTransaction.db.createObjectStore(storeName, storeParams));
} catch (e) {
idbTransaction.readyState !== idbTransaction.DONE && idbTransaction.abort();
}
},
"deleteObjectStore": function(storeName) {
try {
idbTransaction.db.deleteObjectStore(storeName);
} catch (e) {
idbTransaction.readyState !== idbTransaction.DONE && idbTransaction.abort();
}
},
"abort": function() {
idbTransaction.abort();
}
};
},
"objectStore": function(idbObjectStore) {
var result = {};
// Define CRUD operations
var crudOps = ["add", "put", "get", "delete", "clear", "count"];
for (var i = 0; i < crudOps.length; i++) {
result[crudOps[i]] = (function(op) {
return function() {
return wrap.request(function(args) {
return idbObjectStore[op].apply(idbObjectStore, args);
}, arguments);
};
})(crudOps[i]);
}
result.each = function(callback, range, direction) {
return wrap.cursor(function() {
if (direction) {
return idbObjectStore.openCursor(wrap.range(range), direction);
} else {
return idbObjectStore.openCursor(wrap.range(range));
}
}, callback);
};
result.index = function(name) {
return wrap.index(function() {
return idbObjectStore.index(name);
});
};
result.createIndex = function(prop, options, indexName) {
if (arguments.length === 2 && typeof options === "string") {
indexName = arguments[1];
options = null;
}
if (!indexName) {
indexName = prop;
}
return wrap.index(function() {
return idbObjectStore.createIndex(indexName, prop, options);
});
};
result.deleteIndex = function(indexName) {
return idbObjectStore.deleteIndex(indexName);
};
return result;
},
"range": function(r) {
if ($.isArray(r)) {
if (r.length === 1) {
return IDBKeyRange.only(r[0]);
} else {
return IDBKeyRange.bound(r[0], r[1], (typeof r[2] === 'undefined') ? true : r[2], (typeof r[3] === 'undefined') ? true : r[3]);
}
} else if (typeof r === "undefined") {
return null;
} else {
return r;
}
},
"cursor": function(idbCursor, callback) {
return $.Deferred(function(dfd) {
try {
var cursorReq = typeof idbCursor === "function" ? idbCursor() : idbCursor;
cursorReq.onsuccess = function(e) {
if (!cursorReq.result) {
dfd.resolveWith(cursorReq, [null, e]);
return;
}
var elem = {
// Delete, update do not move
"delete": function() {
return wrap.request(function() {
return cursorReq.result["delete"]();
});
},
"update": function(data) {
return wrap.request(function() {
return cursorReq.result["update"](data);
});
},
"next": function(key) {
this.data = key;
},
"key": cursorReq.result.key,
"value": cursorReq.result.value
};
dfd.notifyWith(cursorReq, [elem, e]);
var result = callback.apply(cursorReq, [elem]);
try {
if (result === false) {
dfd.resolveWith(cursorReq, [null, e]);
} else if (typeof result === "number") {
cursorReq.result["advance"].apply(cursorReq.result, [result]);
} else {
if (elem.data) cursorReq.result["continue"].apply(cursorReq.result, [elem.data]);
else cursorReq.result["continue"]();
}
} catch (e) {
dfd.rejectWith(cursorReq, [cursorReq.result, e]);
}
};
cursorReq.onerror = function(e) {
dfd.rejectWith(cursorReq, [cursorReq.result, e]);
};
} catch (e) {
e.type = "exception";
dfd.rejectWith(cursorReq, [null, e]);
}
});
},
"index": function(index) {
try {
var idbIndex = (typeof index === "function" ? index() : index);
} catch (e) {
idbIndex = null;
}
return {
"each": function(callback, range, direction) {
return wrap.cursor(function() {
if (direction) {
return idbIndex.openCursor(wrap.range(range), direction);
} else {
return idbIndex.openCursor(wrap.range(range));
}
}, callback);
},
"eachKey": function(callback, range, direction) {
return wrap.cursor(function() {
if (direction) {
return idbIndex.openKeyCursor(wrap.range(range), direction);
} else {
return idbIndex.openKeyCursor(wrap.range(range));
}
}, callback);
},
"get": function(key) {
if (typeof idbIndex.get === "function") {
return wrap.request(idbIndex.get(key));
} else {
return idbIndex.openCursor(wrap.range(key));
}
},
"count": function() {
if (typeof idbIndex.count === "function") {
return wrap.request(idbIndex.count());
} else {
throw "Count not implemented for cursors";
}
},
"getKey": function(key) {
if (typeof idbIndex.getKey === "function") {
return wrap.request(idbIndex.getKey(key));
} else {
return idbIndex.openKeyCursor(wrap.range(key));
}
}
};
}
};
// Start with opening the database
var dbPromise = wrap.request(function() {
return version ? indexedDB.open(dbName, parseInt(version)) : indexedDB.open(dbName);
});
dbPromise.then(function(db, e) {
db.onversionchange = function() {
// Try to automatically close the database if there is a version change request
if (!(config && config.onversionchange && config.onversionchange() !== false)) {
db.close();
}
};
}, function(error, e) {
// Nothing much to do if an error occurs
}, function(db, e) {
if (e && e.type === "upgradeneeded") {
if (config && config.schema) {
// Assuming that version is always an integer
for (var i = e.oldVersion + 1; i <= e.newVersion; i++) {
typeof config.schema[i] === "function" && config.schema[i].call(this, wrap.transaction(this.transaction));
}
}
if (config && typeof config.upgrade === "function") {
config.upgrade.call(this, wrap.transaction(this.transaction));
}
}
});
return $.extend(dbPromise, {
"cmp": function(key1, key2) {
return indexedDB.cmp(key1, key2);
},
"deleteDatabase": function() {
// Kinda looks ugly coz DB is opened before it needs to be deleted.
// Blame it on the API
return $.Deferred(function(dfd) {
dbPromise.then(function(db, e) {
db.close();
wrap.request(function() {
return indexedDB.deleteDatabase(dbName);
}).then(function(result, e) {
dfd.resolveWith(this, [result, e]);
}, function(error, e) {
dfd.rejectWith(this, [error, e]);
}, function(db, e) {
dfd.notifyWith(this, [db, e]);
});
}, function(error, e) {
dfd.rejectWith(this, [error, e]);
}, function(db, e) {
dfd.notifyWith(this, [db, e]);
});
});
},
"transaction": function(storeNames, mode) {
!$.isArray(storeNames) && (storeNames = [storeNames]);
mode = getDefaultTransaction(mode);
return $.Deferred(function(dfd) {
dbPromise.then(function(db, e) {
var idbTransaction;
try {
idbTransaction = db.transaction(storeNames, mode);
idbTransaction.onabort = idbTransaction.onerror = function(e) {
dfd.rejectWith(idbTransaction, [e]);
};
idbTransaction.oncomplete = function(e) {
dfd.resolveWith(idbTransaction, [e]);
};
} catch (e) {
e.type = "exception";
dfd.rejectWith(this, [e]);
return;
}
try {
dfd.notifyWith(idbTransaction, [wrap.transaction(idbTransaction)]);
} catch (e) {
e.type = "exception";
dfd.rejectWith(this, [e]);
}
}, function(err, e) {
dfd.rejectWith(this, [e, err]);
}, function(res, e) {
//dfd.notifyWith(this, ["", e]);
});
});
},
"objectStore": function(storeName, mode) {
var me = this,
result = {};
function op(callback) {
return $.Deferred(function(dfd) {
function onTransactionProgress(trans, callback) {
try {
callback(trans.objectStore(storeName)).then(function(result, e) {
dfd.resolveWith(this, [result, e]);
}, function(err, e) {
dfd.rejectWith(this, [err, e]);
});
} catch (e) {
e.name = "exception";
dfd.rejectWith(trans, [e, e]);
}
}
me.transaction(storeName, getDefaultTransaction(mode)).then(function() {
// Nothing to do when transaction is complete
}, function(err, e) {
// If transaction fails, CrudOp fails
if (err.code === err.NOT_FOUND_ERR && (mode === true || typeof mode === "object")) {
var db = this.result;
db.close();
dbPromise = wrap.request(function() {
return indexedDB.open(dbName, (parseInt(db.version, 10) || 1) + 1);
});
dbPromise.then(function(db, e) {
db.onversionchange = function() {
// Try to automatically close the database if there is a version change request
if (!(config && config.onversionchange && config.onversionchange() !== false)) {
db.close();
}
};
me.transaction(storeName, getDefaultTransaction(mode)).then(function() {
// Nothing much to do
}, function(err, e) {
dfd.rejectWith(this, [err, e]);
}, function(trans, e) {
onTransactionProgress(trans, callback);
});
}, function(err, e) {
dfd.rejectWith(this, [err, e]);
}, function(db, e) {
if (e.type === "upgradeneeded") {
try {
db.createObjectStore(storeName, mode === true ? {
"autoIncrement": true
} : mode);
} catch (ex) {
dfd.rejectWith(this, [ex, e]);
}
}
});
} else {
dfd.rejectWith(this, [err, e]);
}
}, function(trans) {
onTransactionProgress(trans, callback);
});
});
}
function crudOp(opName, args) {
return op(function(wrappedObjectStore) {
return wrappedObjectStore[opName].apply(wrappedObjectStore, args);
});
}
function indexOp(opName, indexName, args) {
return op(function(wrappedObjectStore) {
var index = wrappedObjectStore.index(indexName);
return index[opName].apply(index[opName], args);
});
}
var crud = ["add", "delete", "get", "put", "clear", "count", "each"];
for (var i = 0; i < crud.length; i++) {
result[crud[i]] = (function(op) {
return function() {
return crudOp(op, arguments);
};
})(crud[i]);
}
result.index = function(indexName) {
return {
"each": function(callback, range, direction) {
return indexOp("each", indexName, [callback, range, direction]);
},
"eachKey": function(callback, range, direction) {
return indexOp("eachKey", indexName, [callback, range, direction]);
},
"get": function(key) {
return indexOp("get", indexName, [key]);
},
"count": function() {
return indexOp("count", indexName, []);
},
"getKey": function(key) {
return indexOp("getKey", indexName, [key]);
}
};
};
return result;
}
});
}
});
$.indexedDB.IDBCursor = IDBCursor;
$.indexedDB.IDBTransaction = IDBTransaction;
$.idb = $.indexedDB;
})(jQuery);