///////////////////////////////////////////////////////////////////////////
// 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,' +
'