diff --git a/lib/amd/build/localstorage.min.js b/lib/amd/build/localstorage.min.js new file mode 100644 index 00000000000..c757939c5b9 --- /dev/null +++ b/lib/amd/build/localstorage.min.js @@ -0,0 +1 @@ +define(["core/config"],function(a){var b=!1,c="",d="",e=null,f=function(){if(-1==a.jsrev)return!1;if("undefined"!=typeof window.localStorage)try{return e=window.localStorage,null!==e}catch(b){return!1}},g=function(a){return c+a},h=function(){var b=e.getItem(d);if(null===b)return void e.setItem(d,a.jsrev);var c=a.jsrev;c!=b&&(e.clear(),e.setItem(d,a.jsrev))},i=function(a){var b,c,d,e=0;if(0===a.length)return e;for(b=0,d=a.length;d>b;b++)c=a.charCodeAt(b),e=(e<<5)-e+c,e|=0;return e},j=function(){b=f();var e=a.wwwroot+"/"+a.jsrev,g=i(e);c=g+"/",e=a.wwwroot+"/",g=i(e),d=g+"/jsrev"};return j(),{get:function(a){return b?(h(),a=g(a),e.getItem(a)):!1},set:function(a,c){if(!b)return!1;h(),a=g(a);try{e.setItem(a,c)}catch(d){return!1}return!0}}}); \ No newline at end of file diff --git a/lib/amd/build/str.min.js b/lib/amd/build/str.min.js index bfdb8260537..ace0e9383c0 100644 --- a/lib/amd/build/str.min.js +++ b/lib/amd/build/str.min.js @@ -1 +1 @@ -define(["jquery","core/ajax"],function(a,b){return{get_string:function(b,c,d,e){var f=a.Deferred(),g=this.get_strings([{key:b,component:c,param:d,lang:e}]);return g.done(function(a){f.resolve(a[0])}).fail(function(a){f.reject(a)}),f.promise()},get_strings:function(c){var d,e=a.Deferred(),f=[],g=0,h=!1;for(g=0;g0&&(e=d.shift().trim()),d.length>0&&(g=d.shift().trim()),d.length>0&&(i=d.join(",").trim());var j=f.imageUrl(e,g),k={attributes:[{name:"src",value:j},{name:"alt",value:i},{name:"class",value:"smallicon"}]},m=h[l+"/core/pix_icon"];return c=a.render(m,k,n),c.trim()},n=function(a){var b="";return t(a,!1).done(function(a){b=a}).fail(e.exception),b},o=function(a,b){return j.push(b(a,this)),""},p=function(a,b){var c=a.split(","),d="",e="",f="";c.length>0&&(d=c.shift().trim()),c.length>0&&(e=c.shift().trim()),c.length>0&&(f=c.join(",").trim()),""!==f&&(f=b(f,this)),0===f.indexOf("{")&&0!==f.indexOf("{{")&&(f=JSON.parse(f));var g=i.length;return i.push({key:d,component:e,param:f}),"{{_s"+g+"}}"},q=function(a,b){l=b,i=[],j=[],a.uniqid=k++,a.str=function(){return p},a.pix=function(){return m},a.js=function(){return o},a.globals={config:g},a.currentTheme=b},r=function(a){var b="";j.length>0&&(b=j.join(";\n"));var c=0;for(c=0;c0?d.get_strings(i).done(function(a){var c;for(c=0;c").attr("type","text/javascript").html(a);b("head").append(c)}}}); \ No newline at end of file +define(["core/mustache","jquery","core/ajax","core/str","core/notification","core/url","core/config","core/localstorage"],function(a,b,c,d,e,f,g,h){var i={},j=[],k=[],l=1,m="",n=function(b){var c,d=b.split(","),e="",g="",h="";d.length>0&&(e=d.shift().trim()),d.length>0&&(g=d.shift().trim()),d.length>0&&(h=d.join(",").trim());var j=f.imageUrl(e,g),k={attributes:[{name:"src",value:j},{name:"alt",value:h},{name:"class",value:"smallicon"}]},l=i[m+"/core/pix_icon"];return c=a.render(l,k,o),c.trim()},o=function(a){var b="";return u(a,!1).done(function(a){b=a}).fail(e.exception),b},p=function(a,b){return k.push(b(a,this)),""},q=function(a,b){var c=a.split(","),d="",e="",f="";c.length>0&&(d=c.shift().trim()),c.length>0&&(e=c.shift().trim()),c.length>0&&(f=c.join(",").trim()),""!==f&&(f=b(f,this)),0===f.indexOf("{")&&0!==f.indexOf("{{")&&(f=JSON.parse(f));var g=j.length;return j.push({key:d,component:e,param:f}),"{{_s"+g+"}}"},r=function(a,b){m=b,j=[],k=[],a.uniqid=l++,a.str=function(){return q},a.pix=function(){return n},a.js=function(){return p},a.globals={config:g},a.currentTheme=b},s=function(a){var b="";k.length>0&&(b=k.join(";\n"));var c=0;for(c=0;c0?d.get_strings(j).done(function(a){var c;for(c=0;c").attr("type","text/javascript").html(a);b("head").append(c)}}}); \ No newline at end of file diff --git a/lib/amd/src/localstorage.js b/lib/amd/src/localstorage.js new file mode 100644 index 00000000000..7cd824e868f --- /dev/null +++ b/lib/amd/src/localstorage.js @@ -0,0 +1,172 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Simple API for set/get to localstorage, with cacherev expiration. + * + * @module core/localstorage + * @package core + * @class localstorage + * @copyright 2015 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since 2.9 + */ +define(['core/config'], function(config) { + + // Private functions and variables. + /** @var {boolean} supported - Is localstorage supported in this browser? */ + var supported = false; + /** @var {string} prefix - Prefix to use on all cache keys */ + var prefix = ''; + /** @var {jsrevPrefix} jsrevPrefix - Key to store the current jsrev version for the cache */ + var jsrevPrefix = ''; + /** @var {Object} localStorage - Browsers localStorage object */ + var localStorage = null; + + /** + * Check if the browser supports local storage. + * + * @method detectSupport + * @return {boolean} True if the browser supports local storage. + */ + var detectSupport = function() { + if (config.jsrev == -1) { + // Disable cache if debugging. + return false; + } + if (typeof(window.localStorage) !== "undefined") { + try { + localStorage = window.localStorage; + return localStorage !== null; + } catch (ex) { + return false; + } + } + }; + + /** + * Add a unique prefix to all keys so multiple moodle sites do not share caches. + * + * @method prefixKey + * @param {string} key The cache key to prefix. + * @return {string} The new key + */ + var prefixKey = function(key) { + return prefix + key; + }; + + /** + * Check the current jsrev version and clear the cache if it has been bumped. + * + * @method validateCache + */ + var validateCache = function() { + var cacheVersion = localStorage.getItem(jsrevPrefix); + if (cacheVersion === null) { + localStorage.setItem(jsrevPrefix, config.jsrev); + return; + } + var moodleVersion = config.jsrev; + + if (moodleVersion != cacheVersion) { + localStorage.clear(); + localStorage.setItem(jsrevPrefix, config.jsrev); + } + }; + + /** + * Hash a string, used to make shorter key prefixes. + * + * @method hashString + * @param string source The string to hash + * @return int The int hash + */ + var hashString = function(source) { + // From http://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript-jquery. + /* jshint bitwise: false */ + var hash = 0, i, chr, len; + if (source.length === 0) { + return hash; + } + for (i = 0, len = source.length; i < len; i++) { + chr = source.charCodeAt(i); + hash = ((hash << 5) - hash) + chr; + hash |= 0; // Convert to 32bit integer + } + return hash; + }; + + /** + * Init this module. + * + * This computes the hash prefixes from jsrev and friends. + */ + var init = function() { + supported = detectSupport(); + var hashSource = config.wwwroot + '/' + config.jsrev; + + var hash = hashString(hashSource); + prefix = hash + '/'; + hashSource = config.wwwroot + '/'; + hash = hashString(hashSource); + jsrevPrefix = hash + '/jsrev'; + }; + + // Run the module init. + init(); + + return /** @alias module:core/localstorage */ { + /** + * Get a value from local storage. Remember - all values must be strings. + * + * @method get + * @param {string} key The cache key to check. + * @return {boolean|string} False if the value is not in the cache, or some other error - a string otherwise. + */ + get: function(key) { + if (!supported) { + return false; + } + validateCache(); + key = prefixKey(key); + + return localStorage.getItem(key); + }, + + /** + * Set a value to local storage. Remember - all values must be strings. + * + * @method set + * @param {string} key The cache key to set. + * @param {string} value The value to set. + * @return {boolean} False if the value can't be saved in the cache, or some other error - true otherwise. + */ + set: function(key, value) { + if (!supported) { + return false; + } + validateCache(); + key = prefixKey(key); + // This can throw exceptions when the storage limit is reached. + try { + localStorage.setItem(key, value); + } catch (e) { + return false; + } + return true; + } + + }; +}); diff --git a/lib/amd/src/str.js b/lib/amd/src/str.js index d342de1b63d..d66c164777f 100644 --- a/lib/amd/src/str.js +++ b/lib/amd/src/str.js @@ -24,7 +24,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 2.9 */ -define(['jquery', 'core/ajax'], function($, ajax) { +define(['jquery', 'core/ajax', 'core/localstorage'], function($, ajax, storage) { return /** @alias module:core/str */ { @@ -42,6 +42,23 @@ define(['jquery', 'core/ajax'], function($, ajax) { get_string: function(key, component, param, lang) { var deferred = $.Deferred(); + if (typeof M.str[component] !== "undefined" && + typeof M.str[component][key] !== "undefined") { + deferred.resolve(M.util.get_string(key, component, param)); + return deferred.promise(); + } + + // Try from local storage. If it's there - put it in M.str and resolve it. + var cached = storage.get('core_str/' + key + '/' + component + '/' + lang); + if (cached) { + if (typeof M.str[component] === "undefined") { + M.str[component] = []; + } + M.str[component][key] = cached; + deferred.resolve(M.util.get_string(key, component, param)); + return deferred.promise(); + } + var request = this.get_strings([{ key: key, component: component, @@ -50,6 +67,7 @@ define(['jquery', 'core/ajax'], function($, ajax) { }]); request.done(function(results) { + storage.set('core_str/' + key + '/' + component + '/' + lang, results[0]); deferred.resolve(results[0]); }).fail(function(ex) { deferred.reject(ex); @@ -73,12 +91,26 @@ define(['jquery', 'core/ajax'], function($, ajax) { var i = 0; var missing = false; var request; + // Try from local storage. If it's there - put it in M.str and resolve it. for (i = 0; i < requests.length; i++) { request = requests[i]; + if (typeof request.lang === "undefined") { + request.lang = $('html').attr('lang'); + } if (typeof M.str[request.component] === "undefined" || typeof M.str[request.component][request.key] === "undefined") { - missing = true; + // Try and revive it from local storage. + var cached = storage.get('core_str/' + request.key + '/' + request.component + '/' + request.lang); + if (cached) { + if (typeof M.str[request.component] === "undefined") { + M.str[request.component] = []; + } + M.str[request.component][request.key] = cached; + } else { + // It's really not here. + missing = true; + } } } @@ -97,9 +129,6 @@ define(['jquery', 'core/ajax'], function($, ajax) { for (i = 0; i < requests.length; i++) { request = requests[i]; - if (typeof request.lang === "undefined") { - request.lang = $('html').attr('lang'); - } ajaxrequests.push({ methodname: 'core_get_string', args: { @@ -123,6 +152,7 @@ define(['jquery', 'core/ajax'], function($, ajax) { M.str[request.component] = []; } M.str[request.component][request.key] = arguments[i]; + storage.set('core_str/' + request.key + '/' + request.component + '/' + request.lang, arguments[i]); // And set the results. results[i] = M.util.get_string(request.key, request.component, request.param).trim(); } diff --git a/lib/amd/src/templates.js b/lib/amd/src/templates.js index 4f581e51ce3..c6be5cc3459 100644 --- a/lib/amd/src/templates.js +++ b/lib/amd/src/templates.js @@ -29,9 +29,10 @@ define([ 'core/mustache', 'core/str', 'core/notification', 'core/url', - 'core/config' + 'core/config', + 'core/localstorage' ], - function(mustache, $, ajax, str, notification, coreurl, config) { + function(mustache, $, ajax, str, notification, coreurl, config, storage) { // Private variables and functions. @@ -268,7 +269,7 @@ define([ 'core/mustache', }; /** - * Load a template from the cache or ajax request. + * Load a template from the cache or local storage or ajax request. * * @method getTemplate * @private @@ -285,28 +286,41 @@ define([ 'core/mustache', var searchKey = currentThemeName + '/' + templateName; + // First try request variables. if (searchKey in templateCache) { deferred.resolve(templateCache[searchKey]); - } else { - var promises = ajax.call([{ - methodname: 'core_output_load_template', - args:{ - component: component, - template: name, - themename: currentThemeName - } - }], async); - promises[0].done( - function (templateSource) { - templateCache[searchKey] = templateSource; - deferred.resolve(templateSource); - } - ).fail( - function (ex) { - deferred.reject(ex); - } - ); + return deferred.promise(); } + + // Now try local storage. + var cached = storage.get('core_template/' + searchKey); + + if (cached) { + deferred.resolve(cached); + return deferred.promise(); + } + + // Oh well - load via ajax. + var promises = ajax.call([{ + methodname: 'core_output_load_template', + args:{ + component: component, + template: name, + themename: currentThemeName + } + }], async); + + promises[0].done( + function (templateSource) { + storage.set('core_template/' + searchKey, templateSource); + templateCache[searchKey] = templateSource; + deferred.resolve(templateSource); + } + ).fail( + function (ex) { + deferred.reject(ex); + } + ); return deferred.promise(); };