/** * Creates a hint handler, displaying hints inside the provided jqHints
. */ (function () { // constants var firstCharacterPos = {'line': 0, 'ch': 0}, sel_no_scroll = {'scroll': false}; codeq.makeHinter = function (jqHints, jqEditor, editor, trNamespace, problemDef, commonDef) { var hintCounter = 0, // for generating unique class-names hintCleaners = [], planIdx = 0, dictionary = [], clearHints = function () { var i; for (i = hintCleaners.length - 1; i >= 0; i--) { hintCleaners[i](); } hintCleaners.length = 0; hintCounter = 0; }, addMark = function (start, end) { var posStart = editor.posFromIndex(start), posEnd = editor.posFromIndex(end), doc = editor.getDoc(), mark = doc.markText(posStart, posEnd, {'className': 'editor-mark _emark_' + hintCounter}), result = {'start': posStart, 'end': posEnd, 'mark': mark, 'className': '_emark_' + hintCounter}; hintCleaners.push(function () { mark.clear(); mark = null; doc = null; result.mark = null; }); hintCounter++; return result; }, processTemplate = function (template, args) { if (!args) return template; return template.replace(/\[%=(\w+)%\]/g, function(match, name) { return args[name].toString() .replace(/&/g, '&') .replace(//g, '>'); }); }, typeHandlers = { 'static': function (type, template, serverHint) { var args = serverHint ? serverHint.args : null, jqContainer, jqButton, i, N, tmpl, tmplIsObject; if (template instanceof Array) { // unwrap the template if there's only one if (template.length == 0) template = ''; else if (template.length == 1) template = template[0] + ''; // it must be a string } if (template instanceof Array) { codeq.log.debug('Processing an array of static hints'); jqContainer = $('
'); jqButton = $(''); jqHints.append(jqContainer); N = template.length; tmpl = template[0]; tmplIsObject = (typeof tmpl === 'object') && (tmpl !== null); jqContainer.append('
' + processTemplate((tmplIsObject ? tmpl.message : tmpl) || '', args) + '
'); jqContainer.append(jqButton); jqButton.text(tmplIsObject && tmpl.linkText ? tmpl.linkText : 'More...'); // TODO: translate "more" i = 1; jqButton.on('click', function () { var tmpl = template[i], tmplIsObject = (typeof tmpl === 'object') && (tmpl !== null), jqNext = $('
' + processTemplate((tmplIsObject ? tmpl.message : tmpl) || '', args) + '
'); i++; if (i < N) { jqButton.before(jqNext); jqButton.text(tmplIsObject && tmpl.linkText ? tmpl.linkText : 'More...'); // TODO: translate "more" } else { jqButton.remove(); jqContainer.append(jqNext); } }); } else { codeq.log.debug('Processing a single static hint'); jqHints.append('
' + processTemplate(template, args) + '
'); } // no hint cleaner here, a static hint remains on the screen }, 'popup': function (type, template, serverHint) { codeq.log.debug('Processing popup hint'); var message = processTemplate(template, serverHint.args), mark = addMark(serverHint.start, serverHint.end), // add the mark jqMark = jqEditor.find('.' + mark.className); jqMark.popover({'content': message, 'html': true, 'placement': 'auto bottom', 'trigger': 'hover focus click', 'container': 'body'}); hintCleaners.push(function () { if (jqMark) { jqMark.popover('destroy'); jqMark = null; } }); mark.mark.on('', function () {}); }, 'dropdown': function (type, template, serverHint) { codeq.log.debug('Processing dropdown hint'); var completion = null, // the completion object, created in showHint() close = function () { if (completion) { completion.close(); completion = null; } }; if (/*(editor.listSelections().length > 1) ||*/ editor.somethingSelected()) { // showHint() doesn't work if a selection is activeparts editor.setSelection(firstCharacterPos, firstCharacterPos, sel_no_scroll); // deselect anything } editor.showHint({ hint: function () { var hints = { list: serverHint.choices, from: editor.posFromIndex(serverHint.start), to: editor.posFromIndex(serverHint.end) }; completion = editor.state.completionActive; return hints; }, completeOnSingleClick: true, completeSingle: false }); hintCleaners.push(close); } }; codeq.tr.registerDictionary(trNamespace, dictionary); var hintProblemDefs = problemDef.hint_type, hintCommonDefs = commonDef.hint_type, hintProblemTr = problemDef.hint, hintCommonTr = commonDef.hint, planDef = problemDef.plan.sl; return { /** * Display the next "planning" hint and return whether there are * any more available. */ 'planNext': function () { if (planIdx < planDef.length) { typeHandlers['static']('static', planDef[planIdx], null); planIdx++; } return planIdx < planDef.length; }, /** * Processes and display appropriately the server hints. * TODO: sort hints so static and popup hints come first, and a (single) drop-down hint last * * @param {ServerHint[]} serverHints an array of hints from the server */ 'handle': function (serverHints) { var n = serverHints.length, i, serverHint, hintId, hintDef, hintContent, hintType, hintTemplate, t, fn, indices; clearHints(); mainLoop: for (i = 0; i < n; i++) { serverHint = serverHints[i]; hintId = serverHint.id; hintDef = hintProblemDefs[hintId]; if (hintDef) { hintContent = hintProblemTr[hintId]; } else { hintDef = hintCommonDefs[hintId]; hintContent = hintCommonTr[hintId]; } if (!hintDef) { codeq.log.error('Undefined hint: ' + hintId); continue; } if (!hintContent) { codeq.log.error('Hint without content: ' + hintId); continue; } if (serverHint.indices) { indices = serverHint.indices; for (i = 0; i < indices.length; i++) { hintContent = hintContent[indices[i]]; if (!hintContent) { codeq.log.error('Cannot reference hint ' + hintId + ' with indices ' + serverHint.indices); continue mainLoop; } } } t = typeof hintDef; if (t === 'string') hintType = hintDef; else if ((t === 'object') && (hintDef !== null)) hintType = hintDef.type; else { codeq.log.error('Cannot determine type of hint ' + hintId + ' from: ' + hintDef); continue; } fn = typeHandlers[hintType]; if (!fn) codeq.log.error('Unsupported hint type: ' + hintType); else fn(hintType, hintContent.sl, serverHint); } }, 'destroy': function () { clearHints(); codeq.tr.unregisterDictionary(trNamespace); jqHints.empty(); jqHints = null; jqEditor = null; editor = null; } }; }; })();