From 3184e7f201af05429bedb4fb8a6300e024946c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Smodi=C5=A1?= Date: Thu, 24 Sep 2015 14:31:13 +0200 Subject: Implemented: CodeQ event queue, basic translation infrastructure, reimplemented problem index from JSON data. --- js/codeq/comms.js | 3 +- js/codeq/core.js | 111 +++++++++++++++++++++++++++++++++++++++- js/codeq/problem.js | 138 +++++++++++++++++++++++++++++++++++++++----------- js/codeq/statusbar.js | 26 ++++++++++ 4 files changed, 246 insertions(+), 32 deletions(-) create mode 100644 js/codeq/statusbar.js (limited to 'js/codeq') diff --git a/js/codeq/comms.js b/js/codeq/comms.js index f9e49ae..2d934bd 100644 --- a/js/codeq/comms.js +++ b/js/codeq/comms.js @@ -169,7 +169,7 @@ ajaxGet = function (url) { return Q.Promise(function (resolve, reject, notify) { $.ajax({ - dataType: 'application/json', + dataType: 'json', type: 'GET', url: url, error: function (jqXHR, textStatus, errorThrown) { @@ -206,6 +206,7 @@ ajaxPrefix[ajaxPrefix.length - 1] = ''; ajaxPrefix = ajaxPrefix.join('/'); } + ajaxPrefix = ajaxPrefix + 'data/'; // ================================================================================ // This module's API methods diff --git a/js/codeq/core.js b/js/codeq/core.js index 61ef607..148cdeb 100644 --- a/js/codeq/core.js +++ b/js/codeq/core.js @@ -147,7 +147,10 @@ regexpWhiteSpaceBeforeTag = new RegExp('[ \r\n\t]+(?=<)', 'g'), regexpWhiteSpaceAfterTag = new RegExp('>[ \r\n\t]+', 'g'), regexpWhiteSpace = new RegExp('[ \\r\\n\\t]+'), - regexpWhiteSpaceTrim = new RegExp('^[ \\t\\r\\n]*(.*[^ \\t\\r\\n])[ \\t\\r\\n]*$', 'm'); + regexpWhiteSpaceTrim = new RegExp('^[ \\t\\r\\n]*(.*[^ \\t\\r\\n])[ \\t\\r\\n]*$', 'm'), + regexpAmp = new RegExp('&', 'g'), + regexpLt = new RegExp('<', 'g'), + regexpGt = new RegExp('>', 'g'); // convert a string into its definition (javascript literal) var stringToDef = function (str) { @@ -160,11 +163,76 @@ return html.replace(regexpWhiteSpaceBeforeTag, '').split(regexpWhiteSpaceAfterTag).join('>'); }; + var lang = 'en'; // this is overridden in the boot sequence below, if the browser uses a supported language + + var eventListeners = {}, // keyed by event name, value is an array of listeners + asyncTimer = null, + queuedEvents = [], + fireEvents = function () { + if (asyncTimer !== null) return; + asyncTimer = setTimeout(function () { + var N = queuedEvents.length, + i, event, args, listeners, j; + asyncTimer = null; + for (i = 0; i < N; i++) { + event = queuedEvents[i]; + listeners = eventListeners[event.name]; + if (!listeners) continue; + args = event.args; + listeners = listeners.slice(); // make a copy of the list, so we're unaffected of any changes + for (j = 0; j < listeners.length; j++) { + try { + listeners[j](args); + } + catch (e) { + codeq.log.error('Error while invoking an event handler for ' + event.name + ': ' + e, e); + } + } + } + queuedEvents.splice(0, N); + if (queuedEvents.length > 0) fireEvents(); + }, 0); + }; + window.codeq = { 'jsonize': jsonize, 'log': log, + 'supportedLangs': { + 'en': 'English', + 'sl': 'Slovenščina' + }, + 'getLang': function () { + return lang; + }, + + 'setLang': function (newLang) { + lang = newLang; + codeq.fire('langchange', {'lang': newLang}); + }, + + 'chooseTranslation': function (translations, language) { + var tr, lang; + if ((typeof translations !== 'object') || (translations === null)) return {}; + tr = translations[language || codeq.lang]; + if ((typeof tr === 'object') && (tr !== null)) return tr; + // default fallback + tr = translations['en']; + if ((typeof tr === 'object') && (tr !== null)) return tr; + // fallback to whatever is available + for (lang in translations) { + tr = translations[lang]; + if ((typeof tr === 'object') && (tr !== null)) return tr; + } + // all options were exhausted, we have nothing + return {}; + }, + + 'escapeHtml': function (s) { + return ('' + s).replace(regexpAmp, '&').replace(regexpLt, '<').replace(regexpGt, '>'); + }, + /** * Returns the number of Unicode code points in the given string. * @@ -262,6 +330,38 @@ if ((typeof args !== 'object') || (args === null)) args = {}; return f.apply(args); }; + }, + + // codeq event handling + + 'fire': function (eventName, args) { + queuedEvents.push({'name': eventName, 'args': args}); + fireEvents(); + }, + + 'on': function (eventName, callback) { + var listeners = eventListeners[eventName], + i; + if (listeners) { + for (i = listeners.length - 1; i >= 0; i--) { + if (listeners[i] === callback) return; // already registered + } + } + else { + listeners = []; + eventListeners[eventName] = listeners; + } + listeners.push(callback); + }, + + 'off': function (eventName, callback) { + var listeners = eventListeners[eventName], + i; + if (listeners) { + for (i = listeners.length - 1; i >= 0; i--) { + if (listeners[i] === callback) listeners.splice(i, 1); + } + } } }; @@ -270,6 +370,15 @@ // ================================================================================ $(document).ready(function () { + // set the language + var lang = navigator.language || navigator.browserLanguage; + if (typeof lang === 'string') { + lang = lang.split('-')[0]; // truncate the language variant, in eg. en-US + if (lang in codeq.supportedLangs) codeq.lang = lang; + } + codeq.setLang(lang); // initial language setting + + // go to login codeq.globalStateMachine.transition('login'); }); })(); diff --git a/js/codeq/problem.js b/js/codeq/problem.js index f19cbd4..fcb5d8a 100644 --- a/js/codeq/problem.js +++ b/js/codeq/problem.js @@ -1,33 +1,112 @@ -/** - * Created by robert on 9/18/15. - */ - -/** - * Created by robert on 9/18/15. - */ - (function(){ - var lastLanguage; + var jqScreen = $('#screen_problem'), + jqLanguageTitle = jqScreen.find('.language-title'), + jqLanguageDescription = jqScreen.find('.language-description'), + jqLanguageProblems = jqScreen.find('.language-problems'), + languageCache = {}, // keyed by language identifier + currentData = null, // currently active data + createLanguageData = function (data, languageIdentifier, lang) { // data is the content of language.json + var trLanguage = codeq.chooseTranslation(data.translations, lang), + groups = data.groups || {}, + languageHints = trLanguage.hint || {}, + html = [], + problemReferences = [], + groupIdentifier, group, trGroup, problems, problemIdentifier, problem, trProblem; + for (groupIdentifier in groups) { + if (!groups.hasOwnProperty(groupIdentifier)) continue; + group = groups[groupIdentifier] || {}; + trGroup = codeq.chooseTranslation(group.translations, lang); + html.push('
  • ', codeq.escapeHtml(trGroup.name || 'Group name not set: ' + groupIdentifier), '
    '); + html.push('
    ', codeq.escapeHtml(trGroup.description || 'Group description not set: ' + groupIdentifier), '
    '); + html.push('
  • '); + } + return { + 'name': trLanguage.name || 'Language name not set: ' + languageIdentifier, + 'description': trLanguage.description || 'Description not set: ' + languageIdentifier, + 'html': html.join(''), + 'refs': problemReferences, + 'hints': trLanguage.hint || {} + }; + }, + onLangChange = function (args) { + purgeDom(); + createDom(currentData); + }, + createDom = function (languageData) { + var lang = codeq.getLang(), + language = languageData.identifier, + data = languageData[lang]; + if (!data) { + data = createLanguageData(languageData.raw, language, lang); + languageData[lang] = data; + } + jqLanguageTitle.html(data.name); + jqLanguageDescription.html(data.description); + jqLanguageProblems.html(data.html); + jqLanguageProblems.find('a').on('click', function () { + var index = +$(this).attr('class').split('-')[1], + ref = data.refs[index]; + if (!ref) { + codeq.log.error('Clicked on a problem link having erroneous index: ' + index); + return; + } + // transition + codeq.wait( + codeq.comms.getProblem(language, ref.g, ref.p) + .then(function (data) { + if (data.code !== 0) throw new Error('Failed to obtain problem data, code: ' + data.code + ', message: ' + data.message); + codeq.globalStateMachine.transition(language, data); + }) + ) + .fail(function (reason) { + codeq.log.error('Failed to obtain the problem definition: ' + reason, reason); + alert('Failed to obtain the problem definition: ' + reason); + }) + .done(); + }); + codeq.on('langchange', onLangChange); + }, + purgeDom = function () { + jqLanguageProblems.find('a').off(); + jqLanguageProblems.empty(); + jqLanguageDescription.empty(); + jqLanguageTitle.empty(); + codeq.off('langchange', onLangChange); + }, + lastLanguage; + codeq.globalStateMachine.register('problem',{ 'enter': function(language){ - if(language)lastLanguage = language; - else language = lastLanguage;//This happens when we hit this with the back button - - $('#disabled').css('display', ''); - $('#disabled').css('cursor', 'wait'); + if (language) lastLanguage = language; + else language = lastLanguage; // This happens when we hit this with the back button $('#navigation-login').css('display', ''); - /*$('#navigation-login').on('click', function(){ - codeq.globalStateMachine.transition('login'); - });*/ $('#navigation-language').css('display', ''); - /*$('#navigation-language').on('click', function(){ - codeq.globalStateMachine.transition('language'); - });*/ - $("#navigation-problem").addClass("active"); - $('#navigation-problem').css('display', ''); + $("#navigation-problem").addClass("active").css('display', ''); + jqScreen.css('display', ''); + + currentData = languageCache[language]; + if (currentData) { + createDom(currentData); + } + else { + codeq.wait(codeq.comms.getLanguageDef(language).then(function (rawData) { + currentData = {'raw': rawData, 'identifier': language}; + languageCache[language] = currentData; + createDom(currentData); + })).done(); + } - codeq.comms.send({'action': 'list_problems', 'language':language}) +/* codeq.comms.send({'action': 'list_problems', 'language':language}) .then( function success(data) { var i, groups, group, problems, problem, first_group, @@ -107,21 +186,20 @@ $('#disabled').css('cursor', ''); alert('Request to obtain list of problems failed: ' + reason); } - ).done(); + ).done();*/ }, 'exit' : function(){ - $('#problem_group').off(); + purgeDom(); +/* $('#problem_group').off(); $("#submit_problem").off(); $("#problem_group option").remove();//empty the selects $("#problems option").remove(); - $("#screen_problem").css('display', 'none'); + $("#screen_problem").css('display', 'none');*/ + jqScreen.css('display', 'none'); $('#navigation-login').css('display', 'none'); - //$('#navigation-login').off(); $('#navigation-language').css('display', 'none'); - //$('#navigation-language').off(); - $('#navigation-problem').css('display', 'none'); - $("#navigation-problem").removeClass("active"); + $('#navigation-problem').css('display', 'none').removeClass("active"); } }); })(); \ No newline at end of file diff --git a/js/codeq/statusbar.js b/js/codeq/statusbar.js new file mode 100644 index 0000000..2134b93 --- /dev/null +++ b/js/codeq/statusbar.js @@ -0,0 +1,26 @@ +(function () { + + var jqBar = $('#topbar'), + jqLang = jqBar.find('.lang-selection'), + jqLangChoice = jqLang.find('.lang-choice'); + + (function () { + var langs = codeq.supportedLangs, + jqMenu = jqBar.find('.dropdown-menu'), + lang, cssClass; + for (lang in langs) { + if (!langs.hasOwnProperty(lang)) continue; + cssClass = 'lang-' + lang; + jqMenu.append('
  • ' + lang + '
  • '); + jqMenu.find('.' + cssClass).on('click', (function (l) {return function () {codeq.setLang(l)};})(lang)); + } + })(); + + codeq.on('langchange', function (params) { + jqLangChoice.text(params.lang); + }); + + codeq.statusbar = { + + }; +})(); \ No newline at end of file -- cgit v1.2.1