/////////////////////////////////////////////////////////////////////////// // MODEL, CONTROLLER SUPPORT /////////////////////////////////////////////////////////////////////////// var $ = require('jquery'), view = require('view'), storage = require('storage'), thumbnail = require('thumbnail'), debug = require('debug'), filetype = require('filetype'), guide = require('guide'), seedrandom = require('seedrandom'), see = require('see'), pencilTracer = require('pencil-tracer'), icedCoffeeScript = require('iced-coffee-script'), drawProtractor = require('draw-protractor'), cache = require('cache'); eval(see.scope('controller')); var model = window.pencilcode.model = { // Owner name of this file or directory. ownername: null, // True if /edit/ url. editmode: false, // Used by framers: an array of extra script for the preview pane, to // scaffold instructional examples. See filetype.js wrapTurtle and // PencilCodeEmbed.setupScript to see how extra scripts are passed through. setupScript: null, // Url used for starting the guide. guideUrl: null, // Contents of the three panes. tempThumbnail: null, pane: { alpha: { filename: null, isdir: false, data: null, bydate: false, loading: 0 }, bravo: { filename: null, isdir: false, data: null, bydate: false, loading: 0 }, charlie: { filename: null, isdir: false, data: null, bydate: false, loading: 0 } }, // Logged in username, or null if not logged in. username: null, // Three digit passkey, hashed from password. passkey: null, // secrets passed in from the embedding frame via // window.location.hash crossFrameContext: getCrossFrameContext() }; function logEvent(name, data) { $.get('/log/' + name, data); } // Log events interesting for academic study: how often code is // run, which code it is, and which mode the editor is in. function logCodeEvent(action, filename, code, mode, lang) { var c = encodeURIComponent(code.substring(0, 1024)). replace(/%20/g, '+').replace(/%0A/g, '|').replace(/%2C/g, ','), m = mode ? 'b' : 't', l = lang ? lang : 'n'; if (l == 'javascript') { l = 'js'; } else if (l == 'coffeescript') { l = 'cs'; } $.get('/log/' + filename + '?' + action + '&mode=' + m + '&lang=' + l + '&code=' + c); } // // Retrieve model.pane object given position. It will be one of // the alpha, bravo or charlie objects from above. // // Parameters: // pos: Position is one of 'left', 'back' or 'right', which maps // to a class name of the element in the html // function modelatpos(pos) { return model.pane[paneatpos(pos)]; } // // Retrieve pane ID corresponding to given position. // // Parameters: // pos: Position is one of 'left', 'back' or 'right', which maps // to a class name of the element in the html // function paneatpos(pos) { return view.paneid(pos); } function posofpane(pane) { return view.panepos(pane); } // // Special owner is defined as one of: // Nobody is the owner of this file/directory OR // it's the guide who's the owner OR // it's the event who's the owner // function specialowner() { return (!model.ownername || model.ownername === 'guide' || model.ownername === 'gymstage' || model.ownername === 'share' || model.ownername === 'example' || model.ownername === 'frame' || model.ownername === 'event'); } // // A no-save owner is an owner that does not participate in saving // or loading at all. This is the case for framed usage. // function nosaveowner() { return model.ownername === 'frame'; } function cansave() { return specialowner() || !model.username || model.tempThumbnail || view.isPaneEditorDirty(paneatpos('left')); } function updateTopControls(addHistory) { var m = modelatpos('left'); // Update visible URL and main title name. view.setNameText(m.filename); var slashed = m.filename; if (m.isdir && slashed.length) { slashed += '/'; } updateVisibleUrl('/edit/' + slashed, model.guideUrl, addHistory) // Update top buttons. var buttons = []; // // If we're not in edit-mode, then push button to enter edit mode // if (!model.editmode) { buttons.push({id: 'editmode', label: 'Edit'}); } else { // // Otherwise check if we have a data file // if (m.data && m.data.file) { // // If so, then insert save button // buttons.push( { id: 'save', title: 'Save program (Ctrl+S)', label: 'Save', menu: [ { id: 'save2', label: 'Save' }, { id: 'saveas', label: 'Copy and Save As...' } ], disabled: !cansave(), }, { id: 'screenshot', title: 'Take screenshot', label: '' }); // Also insert share button if (!specialowner() || !model.ownername) { buttons.push({ id: 'share', title: 'Share links to this program', label: 'Share'}); } } // // If this directory is owned by some person (i.e. not specialowner) // if (!specialowner()) { // Applies to both files and dirs: a simple "new file" button. buttons.push({ id: 'new', title: 'Make a new program', label: 'New'}); // // Then insert logout/login buttons depending on if someone // is already logged in // if (model.username) { buttons.push({ id: 'logout', label: 'Log out', title: 'Log out from ' + model.username}); } else { buttons.push({ id: 'login', label: 'Log in', title: 'Enter password for ' + model.ownername}); } } else { // We're either in some file or directory if (m.isdir) { // // If it's a directory then allow browsing by date // or by alphabetical // if (m.bydate) { buttons.push({id: 'byname', label: 'Alphabetize'}); } else { buttons.push({id: 'bydate', label: 'Sort by Date'}); } } else if (!nosaveowner()) { buttons.push({ id: 'login', label: 'Log in', title: 'Log in and save'}); } } buttons.push( {id: 'help', label: '?' }); if (m.data && m.data.file) { buttons.push({ id: 'guide', label: 'Guide', title: 'Open online guide'}); } // // If this directory has an owner (i.e., not the root owner), // enable splitscreen toggle. // if (model.ownername || m.filename) { buttons.push({ id: 'splitscreen', title: 'Toggle split screen', label: '' }); } } // buttons.push({id: 'done', label: 'Done', title: 'tooltip text'}); view.showButtons(buttons); // Update middle button. if (m.data && m.data.file || (modelatpos('right').data && modelatpos('right').data.file)) { view.showMiddleButton('run'); } else { view.showMiddleButton(''); } // Also if we're runnable, show an empty runner in the right. // Is this helpful or confusing? if (m.data && m.data.file) { if (!modelatpos('right').running) { var doc = view.getPaneEditorData(paneatpos('left')); // The last flag here means: run the supporting scripts // but not the main program. runCodeAtPosition('right', doc, m.filename, true); } } // Update editability. view.setNameTextReadOnly(!model.editmode); view.setPaneEditorReadOnly(paneatpos('right'), true); view.setPaneEditorReadOnly(paneatpos('back'), true); view.setPaneEditorReadOnly(paneatpos('left'), !model.editmode); } // // Set up some logging event handlers. // view.on('selectpalette', function(pane, palname) { if (!palname) { palname = 'default'; } logEvent('~selectpalette', {name: palname.replace(/\s/g, '').toLowerCase()}); }); view.on('pickblock', function(pane, blockid) { logEvent('~pickblock', { id: blockid }); }); // // Now setup event handlers. Each event handler corresponds to // an ID (as specified in updateTopControls() above) and // an event handler function // view.on('help', function() { view.flashNotification('Ask a question.' + (model.username ? '  Change password.' : '') ); }); view.on('tour', function() { // view.flashNotification('Tour coming soon.'); setTimeout(function() { view.flashNotification('Tour coming soon.');}, 0); }); view.on('new', function() { if (modelatpos('left').isdir) { handleDirLink(paneatpos('left'), '#new'); return; } var directoryname = modelatpos('left').filename.replace(/(?:^|\/)[^\/]*$/, '/'); // Load the directory listing to find an unused name. storage.loadFile(model.ownername, directoryname, false, function(m) { var untitled = 'untitled'; if (m.directory && m.list) { untitled = chooseNewFilename(m.list); } if (directoryname == '/') { directoryname = ''; } window.location.href = '/edit/' + directoryname + untitled; }); }); var lastSharedName = ''; view.on('share', function() { var shortfilename = modelatpos('left').filename.replace(/^.*\//, ''); if (!shortfilename) { shortfilename = 'clip'; } var doc = view.getPaneEditorData(paneatpos('left')); if (isEmptyDoc(doc)) { return; } // First save if needed (including login user if necessary) if (view.isPaneEditorDirty(paneatpos('left'))) { saveAction(false, 'Log in to share', shareAction); } else { shareAction(); } function shareAction() { // Then attempt to save on share.pencilcode.net var prefix = (60466175 - (Math.floor((new Date).getTime()/1000) % (24*60*60*500))).toString(36); var sharename = prefix + "-" + model.ownername + "-" + shortfilename.replace(/[^\w\.]+/g, '_').replace(/^_+|_+$/g, ''); if (lastSharedName.substring(prefix.length) == sharename.substring(prefix.length)) { // Don't pollute the shared space with duplicate code; use the // same share filename if the code is the same. sharename = lastSharedName; } if (!doc) { // There is no editor on the left (or it is misbehaving) - do nothing. console.log("Nothing to share."); return; } else if (doc.data !== '') { // If program is not empty, generate thumbnail. if (model.tempThumbnail) { postThumbnailGeneration(model.tempThumbnail); } else { var iframe = document.getElementById('output-frame'); // `thumbnail.generateThumbnailDataUrl` second parameter is a callback. thumbnail.generateThumbnailDataUrl(iframe, postThumbnailGeneration); } } function postThumbnailGeneration(thumbnailDataUrl) { var data = $.extend({ thumbnail: thumbnailDataUrl }, modelatpos('left').data, doc); storage.saveFile('share', sharename, data, true, 828, false, function(m) { var opts = { title: shortfilename }; if (!m.error && !m.deleted) { opts.shareStageURL = "//share." + window.pencilcode.domain + "/home/" + sharename; } if (model.ownername) { // Share the run URL unless there is no owner (e.g., for /first). opts.shareRunURL = "//" + document.domain + '/home/' + modelatpos('left').filename; } opts.shareEditURL = window.location.href; // Now bring up share dialog view.showShareDialog(opts); }); } } }); view.on('fullscreen', function(pane) { function showfullscreen() { var w = window.open("/home/" + model.pane[pane].filename, "run-" + model.ownername); if (!w || w.closed) { view.showDialog({ prompt:'Saved.', content: '

Will open full page.

' + ' ' + '', done: function(s) { s.update({cancel:true}); showfullscreen(); }}); } else { w.focus(); } } if (view.isPaneEditorDirty(paneatpos('left'))) { if (model.ownername == model.username) { // Open immediately to avoid popup blocker. showfullscreen(); } saveAction(false, 'Log in to save', showfullscreen); } else { showfullscreen(); } }); view.on('bydate', function() { if (modelatpos('left').isdir) { modelatpos('left').bydate = true; var pane = paneatpos('left'); updateSortResults(pane); } }); view.on('byname', function() { if (modelatpos('left').isdir) { modelatpos('left').bydate = false; var pane = paneatpos('left'); updateSortResults(pane); } }); view.on('search', function(pane, search, cb) { updateSearchResults(pane, search, cb); }); view.on('dirty', function(pane) { if (posofpane(pane) == 'left') { view.enableButton('save', cansave()); view.enableButton('save2', cansave()); // Toggle button between triangle and refresh. view.showMiddleButton('run'); } }); view.on('changelines', function(pane) { // End debugging session when number of lines is changed. if (posofpane(pane) == 'left') { debug.bindframe(null); } }); view.on('editfocus', function(pane) { if (posofpane(pane) == 'right') { rotateModelLeft(true); } }); view.on('changehtmlcss', function(pane) { if (posofpane(pane) != 'left' || debug.stopButton()) { return; } var doc = view.getPaneEditorData(pane); var newdata = $.extend({}, modelatpos('left').data, doc); var filename = modelatpos('left').filename; runCodeAtPosition('right', newdata, filename, true); view.showMiddleButton('run'); saveDefaultMeta(doc.meta); }); view.on('run', runAction); function runAction() { var doc = view.getPaneEditorData(paneatpos('left')); if (!doc) { doc = view.getPaneEditorData(paneatpos('right')); if (!doc) { console.log('Nothing to run.'); return; } cancelAndClearPosition('back'); rotateModelLeft(true); } if (!view.getPreviewMode()) { view.setPreviewMode(true, true /* no animation */); } // Hide the guide, if any if (guide.isVisible()) { guide.show(false); // Blink the guide button. // view.flashButton('guide'); // Let the animation complete before running. setTimeout(runAction, 500); return; } // Grab the code. var newdata = $.extend({}, modelatpos('left').data, doc); var filename = modelatpos('left').filename; view.clearPaneEditorMarks(paneatpos('left')); if (!specialowner()) { // Save file (backup only) storage.saveFile(model.ownername, filename, newdata, false, null, true); } // Provide instant (momentary) feedback that the program is now running. debug.stopButton('flash'); view.publish('startExecute'); runCodeAtPosition('right', newdata, filename, false); logCodeEvent('run', filename, newdata.data, view.getPaneEditorBlockMode(paneatpos('left')), view.getPaneEditorLanguage(paneatpos('left'))); if (!specialowner()) { // Remember the most recently run program. cookie('recent', window.location.href, { expires: 7, path: '/', domain: window.pencilcode.domain }); } } $(window).on('beforeunload', function() { if (view.isPaneEditorDirty(paneatpos('left')) && !nosaveowner()) { view.flashButton('save'); return "There are unsaved changes." } }); view.on('logout', function() { model.username = null; model.passkey = null; // Erase some cookies after logout. cookie('login', '', { expires: -1, path: '/' }); cookie('recent', '', { expires: -1, path: '/', domain: window.pencilcode.domain }); updateTopControls(false); view.flashNotification('Logged out.'); }); view.on('login', function() { if (specialowner()) { saveAction(false, 'Log in and save.', null); return; } view.showLoginDialog({ prompt: 'Log in.', username: model.ownername, validate: function(state) { return {}; }, switchuser: signUpAndSave, done: function(state) { model.username = model.ownername; model.passkey = keyFromPassword(model.username, state.password); state.update({info: 'Logging in...', disable: true}); storage.setPassKey( model.username, model.passkey, model.passkey, function(m) { if (m.needauth) { state.update({info: 'Wrong password.', disable: false}); model.username = null; model.passkey = null; return; } else if (m.error) { state.update({info: 'Could not log in.', disable: false}); model.username = null; model.passkey = null; return; } state.update({cancel: true}); saveLoginCookie(); if (!specialowner()) { cookie('recent', window.location.href, { expires: 7, path: '/', domain: window.pencilcode.domain }); } updateTopControls(); view.flashNotification('Logged in as ' + model.username + '.'); }); } }); }); view.on('setpass', function() { view.showLoginDialog({ prompt: 'Change password.', username: model.ownername, setpass: true, validate: function(state) { if (state.password === state.newpass) { return { disable: true }; } else { return { disable: false }; } }, done: function(state) { var oldpasskey = keyFromPassword(model.ownername, state.password); var newpasskey = keyFromPassword(model.ownername, state.newpass); state.update({info: 'Changing password...', disable: true}); storage.setPassKey(model.ownername, newpasskey, oldpasskey, function(m) { if (m.needauth) { state.update({info: 'Wrong password.', disable: false}); return; } else if (m.error) { state.update({info: 'Could not change password.', disable: false}); return; } state.update({cancel: true}); model.username = model.ownername; model.passkey = newpasskey; saveLoginCookie(); if (!specialowner()) { cookie('recent', window.location.href, { expires: 7, path: '/', domain: window.pencilcode.domain }); } updateTopControls(); view.flashNotification('Changed password for ' + model.username + '.'); }); } }); }); view.on('screenshot', function() { var iframe = document.getElementById('output-frame'); // `thumbnail.generateThumbnailDataUrl` second parameter is a callback. thumbnail.generateThumbnailDataUrl(iframe, function(thumbnailDataUrl) { model.tempThumbnail = thumbnailDataUrl; updateTopControls(); view.flashThumbnail(thumbnailDataUrl); }); }); view.on('save', function() { saveAction(false, null, null); }); view.on('save2', function() { saveAction(false, null, null); }); view.on('saveas', saveAs); view.on('overwrite', function() { saveAction(true, null, null); }); view.on('guide', function() { if (!model.guideUrl) { window.open( '//guide.' + window.pencilcode.domain + '/home/'); return; } guide.show(!guide.isVisible()); }); guide.on('guideurl', function(guideurl) { readNewUrl.suppress = true; updateVisibleUrl(window.location.pathname, guideurl, false); readNewUrl.suppress = false; }); function guideHash(guideurl) { return guideurl ? '#guide=' + (/[&#%]/.test(guideurl) ? encodeURIComponent(guideurl) : encodeURI(guideurl)) : ''; } function updateVisibleUrl(baseurl, guideurl, addHistory) { model.guideUrl = guideurl; view.setVisibleUrl(baseurl + guideHash(guideurl), addHistory); } guide.on('login', function(options) { if (!options) { options = { oldonly: true, center: true }; } signUpAndSave(options); }); // Used by a guide to set up a starting doc (or a remembered doc). var currentGuideSessionUrl = null; var currentGuideSessionFilename = null; var currentGuideSessionTimer = null; var currentGuideSessionSaveTime = 0; guide.on('session', function session(options) { var url = options.url, match = !url ? null : /^(?:(?:\w+:)?\/\/(\w+)\.\w+[^\/]{8})?(?:\/\w+\/([^#?]*))?$/.exec(url); ownername = match && match[1] || '', filename = match && match[2] || options.filename || 'untitled'; if (options.remove || options.reset) { localStorage.removeItem('pcgs:' + url); if (options.remove) return; } currentGuideSessionUrl = url; currentGuideSessionFilename = filename; // Set up palette if requested. if (options.palette || options.modeOptions) { view.setPaneEditorBlockOptions(paneatpos('left'), options.palette, options.modeOptions); } // Look for session from localStorage var saved = localStorage.getItem('pcgs:' + url); if (saved) { try { saved = JSON.parse(saved); } catch (e) { saved = null; } } if (options && options.age && saved && (!saved.mtime || saved.mtime < (new Date).getTime() - options.age)) { saved = null; } // Do nothing if we are already at the right filename (any user). if (!saved && !options.reset) { var cm = model.pane[paneatpos('left')]; if (filename == cm.filename && cm.data && cm.data.data) { return; } } var doc = $.extend({}, options); if (saved) { $.extend(doc, saved); } // If we have data, load it right away; otherwise load it from the url. if (doc.data != null) { setupEditor(); } else { storage.loadFile(ownername, filename, true, function(loptions) { if (loptions.error) { view.flashNotification(loptions.error); return; } doc = $.extend(loptions, options); setupEditor(); }); } function setupEditor() { var pane = paneatpos('left'); var mpp = model.pane[pane]; if (!doc.file) { doc.file = 'setdoc'; } mpp.isdir = false; mpp.data = doc; var mode = doc.hasOwnProperty('blocks') ? !falsish(doc.blocks) : loadBlockMode(); mpp.filename = filename; mpp.isdir = false; mpp.bydate = false; mpp.loading = nextLoadNumber(); mpp.running = false; view.setPaneEditorData(pane, doc, filename, mode); if (options.palette || options.modeOptions) { view.setPaneEditorBlockOptions(paneatpos('left'), options.palette, options.modeOptions); } updateTopControls(); } }); view.on('delta', function(pane) { // Listen to deltas if there is a guide session active. if (!currentGuideSessionUrl || !currentGuideSessionFilename || currentGuideSessionTimer || posofpane(pane) != 'left' || model.pane[pane].filename != currentGuideSessionFilename) { return; } // Save after every change, polling at most twice per second. var delay = Math.max(0, currentGuideSessionSaveTime + 500 - (new Date).getTime()); currentGuideSessionTimer = setTimeout(function() { currentGuideSessionTimer = null; var doc = view.getPaneEditorData(pane); doc.mtime = currentGuideSessionSaveTime = +(new Date); if (doc && doc.data != null) { localStorage.setItem( 'pcgs:' + currentGuideSessionUrl, JSON.stringify(doc)); } }, delay); }); view.on('toggleblocks', function(p, useblocks) { saveBlockMode(useblocks); var filename = model.pane[p].filename; var doc = view.getPaneEditorData(p), code = (doc && doc.data) || model.pane[p].data.data; logCodeEvent('toggle', filename, code, useblocks, view.getPaneEditorLanguage(p)); }); view.on('splitscreen', function() { view.setPreviewMode(!view.getPreviewMode()); }); function saveAction(forceOverwrite, loginPrompt, doneCallback) { if (nosaveowner()) { return; } if (specialowner()) { var options = {}; if (loginPrompt) { options.prompt = loginPrompt; } signUpAndSave(options); return; } var doc = view.getPaneEditorData(paneatpos('left')); var filename = modelatpos('left').filename; var thumbnailDataUrl = ''; if (!doc) { // There is no editor on the left (or it is misbehaving) - do nothing. console.log("Nothing to save."); return; } else if (doc.data !== '') { // If program is not empty, generate thumbnail if (model.tempThumbnail) { postThumbnailGeneration(model.tempThumbnail); } else { var iframe = document.getElementById('output-frame'); // `thumbnail.generateThumbnailDataUrl` second parameter is a callback. thumbnail.generateThumbnailDataUrl(iframe, postThumbnailGeneration); } } else { // Empty content, file delete, no need for thumbnail. postThumbnailGeneration(''); } function postThumbnailGeneration(thumbnailDataUrl) { // Remember meta in a cookie. saveDefaultMeta(doc.meta); var newdata = $.extend({ thumbnail: thumbnailDataUrl }, modelatpos('left').data, doc); if (newdata.auth && model.ownername != model.username) { // If we know auth is required and the user isn't logged in, // prompt for a login. logInAndSave(filename, newdata, forceOverwrite, noteclean, loginPrompt, doneCallback); return; } // Attempt to save. view.flashNotification('', true); storage.saveFile( model.ownername, filename, newdata, forceOverwrite, model.passkey, false, function(status) { if (status.needauth) { logInAndSave(filename, newdata, forceOverwrite, noteclean, loginPrompt, doneCallback); } else { if (!model.username) { // If not yet logged in but we have saved (e.g., no password needed), // then log us in. model.username = model.ownername; } handleSaveStatus(status, filename, noteclean); if (doneCallback) { doneCallback(); } } }); // After a successful save, mark the file as clean and update mtime. function noteclean(mtime) { view.flashNotification('Saved.'); view.notePaneEditorCleanData(paneatpos('left'), newdata); logCodeEvent('save', filename, newdata.data, view.getPaneEditorBlockMode(paneatpos('left')), view.getPaneEditorLanguage(paneatpos('left'))); if (modelatpos('left').filename == filename) { var oldmtime = modelatpos('left').data.mtime || 0; if (mtime) { modelatpos('left').data.mtime = Math.max(mtime, oldmtime); } } // Delete the pre-saved thumbnail from the model. model.tempThumbnail = null; updateTopControls(); // Flash the thumbnail after the control are updated. view.flashThumbnail(thumbnailDataUrl); } } } function keyFromPassword(username, p) { if (!p) { return ''; } if (/^[0-9]{3}$/.test(p)) { return p; } var key = ''; var prng = seedrandom('turtlebits:' + username + ':' + p + '.'); for (var j = 0; j < 3; j++) { key += Math.floor(prng() * 10); } return key; } function letterComplexity(s) { var maxcount = 0, uniqcount = 0, dupcount = 0, last = null, count = {}, j, c; for (j = 0; j < s.length; ++j) { c = s.charAt(j); if (!(c in count)) { uniqcount += 1; count[c] = 0; } count[c] += 1; maxcount = Math.max(count[c], maxcount); if (c == last) { dupcount += 1; } last = c; } return uniqcount && (uniqcount / (maxcount + dupcount)); } function signUpAndSave(options) { if (!options) { options = {}; } var doc = view.getPaneEditorData(paneatpos('left')); var mp = modelatpos('left'); var shouldCreateAccount = true; if (!doc) { console.log("Nothing to save here."); return; } // updateUserSet will look up only one username at once. It will: // (1) wait until a query has been sitting for 500ms without being // superceded by a newer query; then it will kick off a server // request if the answer to the query isn't already known. // (2) avoid kicking off another server request while one is in // progress and a hasn't returned yet, for up to 10 seconds. // (3) it will restart the process after the server is done // if a new different query has come in the meantime. var userSet = {}; var lastquery = null; var delayTimer = null; var queryTimer = null; function updateUserSet(prefix) { // Repeated queries have no effect. if (lastquery == prefix) return; lastquery = prefix; // A new query will reset the delay timer. clearTimeout(delayTimer); delayTimer = null; // There is no work if the answer is cached. if (userSet.hasOwnProperty(lastquery)) { return; } // Block if a server query is in progress. if (queryTimer) { return; } // Server work starts after a 500ms delay. delayTimer = setTimeout(function() { delayTimer = null; var querying = lastquery; var cancelled = false; // When completed, unblock any newer query once. var complete = function() { if (!cancelled) { cancelled = true; clearTimeout(queryTimer); queryTimer = null; if (!userSet.hasOwnProperty(querying)) { userSet[querying] = 'error'; } // Hackish: trigger a keyup on the $('.username') field to force // a revalidate after we have a userlist. $('.username').trigger('keyup'); if (lastquery != querying) { updateUserSet(lastquery); } } }; // Block other requests for 10 seconds or until the server returns. queryTimer = setTimeout(complete, 10000); // Update the userSet by querying the server. storage.updateUserSet(querying, userSet, complete); }, 500); }; view.showLoginDialog({ prompt: options.prompt || 'Choose an account name to save.', rename: options.nofilename ? '' : (options.filename || mp.filename), center: options.center, cancel: options.cancel, info: 'Accounts on pencilcode are free.', validate: function(state) { var username = state.username.toLowerCase(); shouldCreateAccount = true; var instructions = { disable: true, info: 'Real names are ' + 'not allowed.' + '
When using a Pencil Code account,' + '