<commit_before>var LocalStorage = (function() {
	return {
		clear : function() {
			localStorage.clear();
		}
	};
})();

var LocalDocumentStorage = (function() {
	var prefix = "mindmaps.document.";

	var getDocumentByKey = function(key) {
		var json = localStorage.getItem(key);
		return json ? Document.fromJSON(json) : null;
	};

	// public API
	return {
		/**
		 * Saves a document to the localstorage. Overwrites the old document if
		 * one with the same id exists.
		 *
		 * TODO catch errors (quota exceeded..)
		 */
		saveDocument : function(doc) {
			// update modified date
			doc.dates.modified = new Date();
			localStorage.setItem(prefix + doc.id, doc.serialize());
		},

		/**
		 * Loads a documents from the localstorage.
		 *
		 * @returns the document or null if not found.
		 */
		loadDocument : function(docId) {
			return getDocumentByKey(prefix + docId);
		},

		/**
		 * Gets all documents found in the local storage object.
		 *
		 * @returns an Array of documents
		 */
		getDocuments : function() {
			var documents = [];
			// search localstorage for saved documents
			for ( var i = 0, max = localStorage.length; i < max; i++) {
				var key = localStorage.key(i);
				// value is a document if key confirms to prefix
				if (key.indexOf(prefix) == 0) {
					documents.push(getDocumentByKey(key));
				}
			}
			return documents;
		},

		/**
		 * Deletes a document from the local storage.
		 */
		deleteDocument : function(doc) {
			localStorage.removeItem(prefix + doc.id);
		},

		/**
		 * Deletes all documents from the local storage.
		 */
		deleteAllDocuments : function() {
			_.each(this.getDocuments(), this.deleteDocument);
		}
	};
})();
<commit_msg>added TODO<commit_after>// TODO save to hdd

var LocalStorage = (function() {
	return {
		clear : function() {
			localStorage.clear();
		}
	};
})();

var LocalDocumentStorage = (function() {
	var prefix = "mindmaps.document.";

	var getDocumentByKey = function(key) {
		var json = localStorage.getItem(key);
		return json ? Document.fromJSON(json) : null;
	};

	// public API
	return {
		/**
		 * Saves a document to the localstorage. Overwrites the old document if
		 * one with the same id exists.
		 *
		 * TODO catch errors (quota exceeded..)
		 */
		saveDocument : function(doc) {
			// update modified date
			doc.dates.modified = new Date();
			localStorage.setItem(prefix + doc.id, doc.serialize());
		},

		/**
		 * Loads a documents from the localstorage.
		 *
		 * @returns the document or null if not found.
		 */
		loadDocument : function(docId) {
			return getDocumentByKey(prefix + docId);
		},

		/**
		 * Gets all documents found in the local storage object.
		 *
		 * @returns an Array of documents
		 */
		getDocuments : function() {
			var documents = [];
			// search localstorage for saved documents
			for ( var i = 0, max = localStorage.length; i < max; i++) {
				var key = localStorage.key(i);
				// value is a document if key confirms to prefix
				if (key.indexOf(prefix) == 0) {
					documents.push(getDocumentByKey(key));
				}
			}
			return documents;
		},

		/**
		 * Deletes a document from the local storage.
		 */
		deleteDocument : function(doc) {
			localStorage.removeItem(prefix + doc.id);
		},

		/**
		 * Deletes all documents from the local storage.
		 */
		deleteAllDocuments : function() {
			_.each(this.getDocuments(), this.deleteDocument);
		}
	};
})();
<|endoftext|>


<commit_before>"use strict";

var path = require('path');
var fs = require('fs');
var findit = require('findit');
var _ = require('underscore');

var TAGIT_DIR = '.tagit';
var TAGIT_FILE = 'data.json';

/**
 * Initializes the working directory
 *
 * @param root
 * @param  {Function} cb
 */
exports.init = function init(root, cb) {
    var dir = path.join(root, TAGIT_DIR);
    if (!fs.existsSync(dir)) {
        try {
            fs.mkdirSync(dir);
            save(root, {});
            cb(undefined, true);
        }
        catch (ex) {
            cb(ex);
        }
    }
    else {
        cb(undefined, false)
    }
};

exports.update = function update(root) {
    var finder = findit(root);
    var data = load(root);
    finder.on('file', function (file) {
        add(data, file);
    });
    finder.on('end', function () {
        save(root, data);
    });
};

exports.tag = function tag(root, f, tags) {
    var data = load(root);
    data = add(data, f, tags);
    save(root, data);
};

exports.tags = function tags(root, f) {
    var data = load(root);
    return data.files[f].tags || [];
};

exports.tagged = function tagged(root, tags) {
    var data = load(root);
    var matchingFiles = [];
    _.each(data.files, function (f) {
        if (f.tags) {
            if (_.intersection(f.tags, tags).length === tags.length) {
                matchingFiles.push(f);
            }
        }
    });
    return matchingFiles;
};

exports.random = function random(root, tags) {
    return _.sample(exports.tagged(root, tags));
};

// Private functions

function load(root) {
    var dataPath = path.join(root, TAGIT_DIR, TAGIT_FILE);
    return JSON.parse(fs.readFileSync(dataPath, 'utf8'));
}

function save(root, data) {
    var dataPath = path.join(root, TAGIT_DIR, TAGIT_FILE);
    fs.writeFileSync(dataPath, JSON.stringify(data), 'utf8');
}

function add(data, f, tags) {
    tags = tags || [];
    data.files = data.files || {};
    data.files[f] = data.files[f] || {};
    var previousTags = data.files[f].tags || [];
    var allTags = _.uniq(tags.concat(previousTags));
    data.files[f] = {'name': f, 'tags': allTags, 'added': Date.now()};
    return data;
}

<commit_msg>Handle errors when traversing directory structure<commit_after>"use strict";

var path = require('path');
var fs = require('fs');
var findit = require('findit');
var _ = require('underscore');

var TAGIT_DIR = '.tagit';
var TAGIT_FILE = 'data.json';

/**
 * Initializes the working directory
 *
 * @param root
 * @param  {Function} cb
 */
exports.init = function init(root, cb) {
    var dir = path.join(root, TAGIT_DIR);
    if (!fs.existsSync(dir)) {
        try {
            fs.mkdirSync(dir);
            save(root, {});
            cb(undefined, true);
        }
        catch (ex) {
            cb(ex);
        }
    }
    else {
        cb(undefined, false)
    }
};

exports.update = function update(root) {
    var finder = findit(root);
    var data = load(root);
    finder.on('file', function (f) {
        if (f.indexOf('.') !== 0) {
            add(data, f);
        }
    });

    finder.on('end', function () {
        save(root, data);
    });

    finder.on('error', function (err) {
        console.warn("processing %s: %s", err.path, err.code);
    })
};

exports.tag = function tag(root, f, tags) {
    var data = load(root);
    data = add(data, f, tags);
    save(root, data);
};

exports.tags = function tags(root, f) {
    var data = load(root);
    return data.files[f].tags || [];
};

exports.tagged = function tagged(root, tags) {
    var data = load(root);
    var matchingFiles = [];
    _.each(data.files, function (f) {
        if (f.tags) {
            if (_.intersection(f.tags, tags).length === tags.length) {
                matchingFiles.push(f);
            }
        }
    });
    return matchingFiles;
};

exports.random = function random(root, tags) {
    return _.sample(exports.tagged(root, tags));
};

// Private functions

function load(root) {
    var dataPath = path.join(root, TAGIT_DIR, TAGIT_FILE);
    return JSON.parse(fs.readFileSync(dataPath, 'utf8'));
}

function save(root, data) {
    var dataPath = path.join(root, TAGIT_DIR, TAGIT_FILE);
    fs.writeFileSync(dataPath, JSON.stringify(data), 'utf8');
}

function add(data, f, tags) {
    tags = tags || [];
    data.files = data.files || {};
    data.files[f] = data.files[f] || {};
    var previousTags = data.files[f].tags || [];
    var allTags = _.uniq(tags.concat(previousTags));
    data.files[f] = {'name': f, 'tags': allTags, 'added': Date.now()};
    return data;
}

<|endoftext|>


<commit_before>const spreadsheetUrl = (k) =>
  `https://spreadsheets.google.com/feeds/list/${k}/1/public/basic?alt=json`;

export default {
  t201706: {
    id: "t201706",
    title: "Le performance come esperienza religiosa",
    speaker: "Andrea De Carolis",
    twitter: "@decarola",
    date: "27 Giugno",
    form: "https://goo.gl/pOigK2",
    spreadsheet: spreadsheetUrl("1j3-BnbqDxv1czcfVFr4d0fyMrHMjT3EZz_j8uMBfni0"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201709: {
    id: "t201709",
    title: "Inspecting, debugging and profiling con Chrome Developer Tools",
    speaker: "Giacomo Zinetti",
    twitter: "@gcmznt",
    date: "26 Settembre",
    form: "https://goo.gl/2QfCqd",
    spreadsheet: spreadsheetUrl("1IqfOkj1TfGrrEB3pJfudlW6BkzuJduGynosQ2gDtqtI"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201710: {
    id: "t201710",
    title: "Refactoring CSS",
    speaker: "Mattia Tommasone",
    twitter: "@raibaz",
    date: "31 Ottobre",
    form: "https://goo.gl/abhUzx",
    spreadsheet: spreadsheetUrl("1yNfKX3eZcPdW3o5xWiZKRRdGw2Uqo4IjMGD0s6qFg90"),
    prices: [
      "jetbrain license",
      "jetbrain license",
      "codemotion ticket",
      "tshirt",
      "tshirt",
      "tshirt",
      "tshirt",
      "tshirt",
      "tshirt",
      "tshirt"
    ]
  },
  t201711: {
    id: "t201711",
    title: "Scrum Wars: Manuale di sopravvivenza agilep er frontenders",
    speaker: "Simone Lelli",
    twitter: "@simonelelli",
    date: "28 Novembre",
    form: "https://goo.gl/1EpvDV",
    spreadsheet: spreadsheetUrl("1ClpVr_TBdCgVAwUv4uYm68_obeyr4N8gw84bRHFhpik"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201712: {
    id: "t201712",
    title: "What Service Workers can do",
    speaker: "Maurizio Mangione",
    twitter: "@granze",
    date: "19 Dicembre",
    form: "https://goo.gl/2Kgpxx",
    spreadsheet: spreadsheetUrl("1iJpYSsRDlXKLcCDbHyxnALwi7JDhrDzZuYSUN_wqhRs"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201801: {
    id: "t201801",
    title: "Let's make frontend fun again!",
    speaker: "Salvatore Laisa",
    twitter: "@moebiusmania",
    date: "30 Gennaio",
    form: "https://goo.gl/g2AFBH",
    spreadsheet: spreadsheetUrl("1FUdcbYTzJqs2_i6qKHwmPBLyPlJX4VbJTjP9QQjIKBM"),
    prices: ["jetbrain license", "jetbrain license", "React Amsterdam"]
  },
  t201802: {
    id: "t201802",
    title: "Atomic Design: Pensare a Componenti Modulari",
    speaker: "Paul Jesus Benites Romero",
    twitter: "@pjbenitesromero",
    date: "27 Febbraio",
    form: "https://goo.gl/9ZfhxL",
    spreadsheet: spreadsheetUrl("1gV9N7pFQvHIL3pDWm8w7Wjcrh9nZe4nACU82xPRiFGo"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201803: {
    id: "t201803",
    title: "Incapsulamento e theming: le nuove frontiere dell'ultima sfida CSS",
    speaker: "Massimo Artizzu",
    twitter: "@MaxArt2501",
    date: "27 Marzo",
    form: "https://goo.gl/X7pjW2",
    spreadsheet: spreadsheetUrl("1gWUvVEP54v6q-AOOBjCwfXoZShgm98WiRcfhWrshYYo"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201804: {
    id: "t201804",
    title: "Super fast transitions",
    speaker: "Giacomo Zinetti",
    twitter: "@gcmznt",
    date: "24 Aprile",
    form: "https://goo.gl/p8uDC5",
    spreadsheet: spreadsheetUrl("1IiQsmeiLVSPX0tGFnoO_C-IHwSLJmpiI3IQZsdR0fqg"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201805: {
    id: "t201805",
    title: "Mystery Box",
    speaker: "Davide Di Pumpo",
    twitter: "@MakhBeth",
    date: "29 Maggio",
    form: "https://goo.gl/u7ghDn",
    spreadsheet: spreadsheetUrl("14naqcmlkQtiVsl8cx4nn5rAolifQMLWWfAw5O-eLSAI"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201806: {
    id: "t201806",
    title: "Deep dive into oceanic javascript animations with a greensock",
    speaker: "Alessandro Rigobello",
    twitter: "@sndrgb",
    date: "26 Giugno",
    form: "https://goo.gl/2yZG6N",
    spreadsheet: spreadsheetUrl("1r4cE2vWHvIwq_tRZ_VjCveUZe6RSgwPduJWsoAT8CSk"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201809: {
    id: "t201809",
    title: "Awesome Vue Vol.1",
    speaker: "Claudio Bisconti",
    twitter: "@claudiobisconti",
    date: "24 Settembre",
    form: "https://goo.gl/ddHv3t",
    spreadsheet: spreadsheetUrl("1ZUtBZpLjE1fqZidLMF9yCaa3ChD6hSt0r9mzHSfmX_4"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201810: {
    id: "t201810",
    title: "Awesome Vue Vol.2",
    speaker: "Claudio Bisconti",
    twitter: "@claudiobisconti",
    date: "23 Ottobre",
    form: "https://goo.gl/GiAMsh",
    spreadsheet: spreadsheetUrl("1_-7gfq2xFOxJsWiG2obmym-9PBp31Io57N7Y3O1PlZg"),
    prices: [
      "jetbrain license",
      "jetbrain license",
      "codemotion ticket",
      "tshirt",
      "tshirt",
      "tshirt",
      "tshirt",
      "tshirt",
      "tshirt",
      "tshirt",
      "tshirt",
      "tshirt"
    ]
  },
  t201811: {
    id: "t201811",
    title: "Il Rinascimento del CSS",
    speaker: "Jiayi Hu",
    twitter: "@jiayi_ghu",
    date: "27 Novembre",
    form: "https://goo.gl/Y77G9E",
    spreadsheet: spreadsheetUrl("1ufyX7unzB62yNH0raZvxVZtTboIJTkNPWe9GGH_3o3A"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201812: {
    id: "t201812",
    title: "Se Gutenberg fosse nato oggi",
    speaker: "Giulia Talamonti",
    twitter: "@giucot_",
    date: "18 Dicembre",
    form: "https://bit.ly/2EvSzwB",
    spreadsheet: spreadsheetUrl("130DQoiGJCrOl7rTDAYKaUWeZ7mRY2QGzLThOwieenqY"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201901: {
    id: "t201901",
    title: "Di test end to end e Cypress",
    speaker: "Daniele Di Lucido",
    twitter: "@DanieleDiLucido",
    date: "22 Gennaio",
    form: "https://bit.ly/2RGk8eg",
    spreadsheet: spreadsheetUrl("1ewryoQF776hEIY2DPF700rjcAOed7M_VsEvgq2XPtwk"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201902: {
    id: "t201902",
    title: "GatsbyJS: il futuro è veloce",
    speaker: "Marco Valsecchi",
    twitter: "@valse",
    date: "26 Febbraio",
    form: "https://bit.ly/2TnHIMK",
    spreadsheet: spreadsheetUrl("1v587IDlWd1e3P4kGvO4ej7WwnkjXTPOUN3sOxfZ3TzQ"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201903: {
    id: "t201903",
    title: "SOLID principles & Apollo GraphQL",
    speaker: "Erdogan Tufa & Przemysław Fałowski",
    twitter: "",
    date: "27 Marzo",
    form: "https://bit.ly/2Wr8nGo",
    spreadsheet: spreadsheetUrl("1ZiCu588y_3eHrRlhkitu_eJCr9NBBHA9G_EGgY9mqxw"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201904: {
    id: "t201904",
    title: "Test E2E da Cypress",
    speaker: "Stefano Magni",
    twitter: "@NoriSte",
    date: "30 Aprile",
    form: "http://bit.ly/2LcRm27",
    spreadsheet: spreadsheetUrl("1m7_vWNUn6g-1pnI1FaJO_WXoAYuExTBJcHAToDNbI-w"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201905: {
    id: "t201905",
    title: "What's next in frontend-land?",
    speaker: "Salvatore Laisa",
    twitter: "@moebiusmania",
    date: "21 Maggio",
    form: "http://bit.ly/2QbpH08",
    spreadsheet: spreadsheetUrl("1u7pU5IlWo6Doem41SitXKc8Rt1S4KWfuaaD6xbQnxRI"),
    prices: ["jetbrain license", "jetbrain license", "serverless", "serverless"]
  },
  t201906: {
    id: "t201906",
    title: "Svelte 3, the React killer (?)",
    speaker: "Valentino Gagliardi",
    twitter: "@gagliardi_vale",
    date: "25 Giugno",
    form: "http://bit.ly/2FxtL73",
    spreadsheet: spreadsheetUrl("12bMraRvreyGDQNoQfhesC85j4S1jHfO_wlvlwDzeYE8"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  current: {
    id: "t201909",
    title: "Una passeggiata tra cetrioli, caffè e test e2e",
    speaker: "Davide Di Pumpo",
    twitter: "@makhbeth",
    date: "30 Settembre",
    form: "http://bit.ly/2nVGOcg",
    spreadsheet: spreadsheetUrl("11n9XpbewfY4iSA0YePA2-Jyi_fxWIx6D0AqOWRXrsKI"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  next: {
    id: "next",
    title: "??!!!?",
    speaker: "Forse tu?",
    twitter: "",
    date: "Ultimo martedi del mese",
    form: "",
    spreadsheet: spreadsheetUrl(""),
    prices: ["jetbrain license", "jetbrain license"]
  }
};
<commit_msg>ottobre 2019<commit_after>const spreadsheetUrl = (k) =>
  `https://spreadsheets.google.com/feeds/list/${k}/1/public/basic?alt=json`;

export default {
  t201706: {
    id: "t201706",
    title: "Le performance come esperienza religiosa",
    speaker: "Andrea De Carolis",
    twitter: "@decarola",
    date: "27 Giugno",
    form: "https://goo.gl/pOigK2",
    spreadsheet: spreadsheetUrl("1j3-BnbqDxv1czcfVFr4d0fyMrHMjT3EZz_j8uMBfni0"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201709: {
    id: "t201709",
    title: "Inspecting, debugging and profiling con Chrome Developer Tools",
    speaker: "Giacomo Zinetti",
    twitter: "@gcmznt",
    date: "26 Settembre",
    form: "https://goo.gl/2QfCqd",
    spreadsheet: spreadsheetUrl("1IqfOkj1TfGrrEB3pJfudlW6BkzuJduGynosQ2gDtqtI"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201710: {
    id: "t201710",
    title: "Refactoring CSS",
    speaker: "Mattia Tommasone",
    twitter: "@raibaz",
    date: "31 Ottobre",
    form: "https://goo.gl/abhUzx",
    spreadsheet: spreadsheetUrl("1yNfKX3eZcPdW3o5xWiZKRRdGw2Uqo4IjMGD0s6qFg90"),
    prices: [
      "jetbrain license",
      "jetbrain license",
      "codemotion ticket",
      "tshirt",
      "tshirt",
      "tshirt",
      "tshirt",
      "tshirt",
      "tshirt",
      "tshirt"
    ]
  },
  t201711: {
    id: "t201711",
    title: "Scrum Wars: Manuale di sopravvivenza agilep er frontenders",
    speaker: "Simone Lelli",
    twitter: "@simonelelli",
    date: "28 Novembre",
    form: "https://goo.gl/1EpvDV",
    spreadsheet: spreadsheetUrl("1ClpVr_TBdCgVAwUv4uYm68_obeyr4N8gw84bRHFhpik"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201712: {
    id: "t201712",
    title: "What Service Workers can do",
    speaker: "Maurizio Mangione",
    twitter: "@granze",
    date: "19 Dicembre",
    form: "https://goo.gl/2Kgpxx",
    spreadsheet: spreadsheetUrl("1iJpYSsRDlXKLcCDbHyxnALwi7JDhrDzZuYSUN_wqhRs"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201801: {
    id: "t201801",
    title: "Let's make frontend fun again!",
    speaker: "Salvatore Laisa",
    twitter: "@moebiusmania",
    date: "30 Gennaio",
    form: "https://goo.gl/g2AFBH",
    spreadsheet: spreadsheetUrl("1FUdcbYTzJqs2_i6qKHwmPBLyPlJX4VbJTjP9QQjIKBM"),
    prices: ["jetbrain license", "jetbrain license", "React Amsterdam"]
  },
  t201802: {
    id: "t201802",
    title: "Atomic Design: Pensare a Componenti Modulari",
    speaker: "Paul Jesus Benites Romero",
    twitter: "@pjbenitesromero",
    date: "27 Febbraio",
    form: "https://goo.gl/9ZfhxL",
    spreadsheet: spreadsheetUrl("1gV9N7pFQvHIL3pDWm8w7Wjcrh9nZe4nACU82xPRiFGo"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201803: {
    id: "t201803",
    title: "Incapsulamento e theming: le nuove frontiere dell'ultima sfida CSS",
    speaker: "Massimo Artizzu",
    twitter: "@MaxArt2501",
    date: "27 Marzo",
    form: "https://goo.gl/X7pjW2",
    spreadsheet: spreadsheetUrl("1gWUvVEP54v6q-AOOBjCwfXoZShgm98WiRcfhWrshYYo"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201804: {
    id: "t201804",
    title: "Super fast transitions",
    speaker: "Giacomo Zinetti",
    twitter: "@gcmznt",
    date: "24 Aprile",
    form: "https://goo.gl/p8uDC5",
    spreadsheet: spreadsheetUrl("1IiQsmeiLVSPX0tGFnoO_C-IHwSLJmpiI3IQZsdR0fqg"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201805: {
    id: "t201805",
    title: "Mystery Box",
    speaker: "Davide Di Pumpo",
    twitter: "@MakhBeth",
    date: "29 Maggio",
    form: "https://goo.gl/u7ghDn",
    spreadsheet: spreadsheetUrl("14naqcmlkQtiVsl8cx4nn5rAolifQMLWWfAw5O-eLSAI"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201806: {
    id: "t201806",
    title: "Deep dive into oceanic javascript animations with a greensock",
    speaker: "Alessandro Rigobello",
    twitter: "@sndrgb",
    date: "26 Giugno",
    form: "https://goo.gl/2yZG6N",
    spreadsheet: spreadsheetUrl("1r4cE2vWHvIwq_tRZ_VjCveUZe6RSgwPduJWsoAT8CSk"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201809: {
    id: "t201809",
    title: "Awesome Vue Vol.1",
    speaker: "Claudio Bisconti",
    twitter: "@claudiobisconti",
    date: "24 Settembre",
    form: "https://goo.gl/ddHv3t",
    spreadsheet: spreadsheetUrl("1ZUtBZpLjE1fqZidLMF9yCaa3ChD6hSt0r9mzHSfmX_4"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201810: {
    id: "t201810",
    title: "Awesome Vue Vol.2",
    speaker: "Claudio Bisconti",
    twitter: "@claudiobisconti",
    date: "23 Ottobre",
    form: "https://goo.gl/GiAMsh",
    spreadsheet: spreadsheetUrl("1_-7gfq2xFOxJsWiG2obmym-9PBp31Io57N7Y3O1PlZg"),
    prices: [
      "jetbrain license",
      "jetbrain license",
      "codemotion ticket",
      "tshirt",
      "tshirt",
      "tshirt",
      "tshirt",
      "tshirt",
      "tshirt",
      "tshirt",
      "tshirt",
      "tshirt"
    ]
  },
  t201811: {
    id: "t201811",
    title: "Il Rinascimento del CSS",
    speaker: "Jiayi Hu",
    twitter: "@jiayi_ghu",
    date: "27 Novembre",
    form: "https://goo.gl/Y77G9E",
    spreadsheet: spreadsheetUrl("1ufyX7unzB62yNH0raZvxVZtTboIJTkNPWe9GGH_3o3A"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201812: {
    id: "t201812",
    title: "Se Gutenberg fosse nato oggi",
    speaker: "Giulia Talamonti",
    twitter: "@giucot_",
    date: "18 Dicembre",
    form: "https://bit.ly/2EvSzwB",
    spreadsheet: spreadsheetUrl("130DQoiGJCrOl7rTDAYKaUWeZ7mRY2QGzLThOwieenqY"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201901: {
    id: "t201901",
    title: "Di test end to end e Cypress",
    speaker: "Daniele Di Lucido",
    twitter: "@DanieleDiLucido",
    date: "22 Gennaio",
    form: "https://bit.ly/2RGk8eg",
    spreadsheet: spreadsheetUrl("1ewryoQF776hEIY2DPF700rjcAOed7M_VsEvgq2XPtwk"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201902: {
    id: "t201902",
    title: "GatsbyJS: il futuro è veloce",
    speaker: "Marco Valsecchi",
    twitter: "@valse",
    date: "26 Febbraio",
    form: "https://bit.ly/2TnHIMK",
    spreadsheet: spreadsheetUrl("1v587IDlWd1e3P4kGvO4ej7WwnkjXTPOUN3sOxfZ3TzQ"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201903: {
    id: "t201903",
    title: "SOLID principles & Apollo GraphQL",
    speaker: "Erdogan Tufa & Przemysław Fałowski",
    twitter: "",
    date: "27 Marzo",
    form: "https://bit.ly/2Wr8nGo",
    spreadsheet: spreadsheetUrl("1ZiCu588y_3eHrRlhkitu_eJCr9NBBHA9G_EGgY9mqxw"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201904: {
    id: "t201904",
    title: "Test E2E da Cypress",
    speaker: "Stefano Magni",
    twitter: "@NoriSte",
    date: "30 Aprile",
    form: "http://bit.ly/2LcRm27",
    spreadsheet: spreadsheetUrl("1m7_vWNUn6g-1pnI1FaJO_WXoAYuExTBJcHAToDNbI-w"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201905: {
    id: "t201905",
    title: "What's next in frontend-land?",
    speaker: "Salvatore Laisa",
    twitter: "@moebiusmania",
    date: "21 Maggio",
    form: "http://bit.ly/2QbpH08",
    spreadsheet: spreadsheetUrl("1u7pU5IlWo6Doem41SitXKc8Rt1S4KWfuaaD6xbQnxRI"),
    prices: ["jetbrain license", "jetbrain license", "serverless", "serverless"]
  },
  t201906: {
    id: "t201906",
    title: "Svelte 3, the React killer (?)",
    speaker: "Valentino Gagliardi",
    twitter: "@gagliardi_vale",
    date: "25 Giugno",
    form: "http://bit.ly/2FxtL73",
    spreadsheet: spreadsheetUrl("12bMraRvreyGDQNoQfhesC85j4S1jHfO_wlvlwDzeYE8"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  t201909: {
    id: "t201909",
    title: "Una passeggiata tra cetrioli, caffè e test e2e",
    speaker: "Davide Di Pumpo",
    twitter: "@makhbeth",
    date: "30 Settembre",
    form: "http://bit.ly/2nVGOcg",
    spreadsheet: spreadsheetUrl("11n9XpbewfY4iSA0YePA2-Jyi_fxWIx6D0AqOWRXrsKI"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  current: {
    id: "t201910",
    title: "TypeScript, React and you",
    speaker: "Gabriele Petronella",
    twitter: "@gabro27",
    date: "22 Ottobre",
    form: "http://bit.ly/35YxS7t",
    spreadsheet: spreadsheetUrl("1fJhElM8j3jEzuEuTGGA4tlZh7CKAhGbNlaLWFhuReqI"),
    prices: ["jetbrain license", "jetbrain license"]
  },
  next: {
    id: "next",
    title: "??!!!?",
    speaker: "Forse tu?",
    twitter: "",
    date: "Ultimo martedi del mese",
    form: "",
    spreadsheet: spreadsheetUrl(""),
    prices: ["jetbrain license", "jetbrain license"]
  }
};
<|endoftext|>


<commit_before>/* ========================================================================
 * Bootstrap: tooltip.js v3.1.1
 * http://getbootstrap.com/javascript/#tooltip
 * Inspired by the original jQuery.tipsy by Jason Frame
 * ========================================================================
 * Copyright 2011-2014 Twitter, Inc.
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 * ======================================================================== */

+function () { 'use strict';

  (function (o_o) {
    typeof define  == 'function' && define.amd ? define(['jquery'], o_o) :
    typeof exports == 'object' ? o_o(require('jquery')) : o_o(jQuery)
  })(function ($) {

    // TOOLTIP PUBLIC CLASS DEFINITION
    // ===============================

    var Tooltip = function (element, options) {
      this.type       =
      this.options    =
      this.enabled    =
      this.timeout    =
      this.hoverState =
      this.$element   = null

      this.init('tooltip', element, options)
    }

    Tooltip.VERSION  = '3.1.1'

    Tooltip.DEFAULTS = {
      animation: true,
      placement: 'top',
      selector: false,
      template: '

',
      trigger: 'hover focus',
      title: '',
      delay: 0,
      html: false,
      container: false,
      viewport: {
        selector: 'body',
        padding: 0
      }
    }

    Tooltip.prototype.init = function (type, element, options) {
      this.enabled   = true
      this.type      = type
      this.$element  = $(element)
      this.options   = this.getOptions(options)
      this.$viewport = this.options.viewport && $(this.options.viewport.selector || this.options.viewport)

      var triggers = this.options.trigger.split(' ')

      for (var i = triggers.length; i--;) {
        var trigger = triggers[i]

        if (trigger == 'click') {
          this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
        } else if (trigger != 'manual') {
          var eventIn  = trigger == 'hover' ? 'mouseenter' : 'focusin'
          var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout'

          this.$element.on(eventIn  + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
          this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
        }
      }

      this.options.selector ?
        (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
        this.fixTitle()
    }

    Tooltip.prototype.getDefaults = function () {
      return Tooltip.DEFAULTS
    }

    Tooltip.prototype.getOptions = function (options) {
      options = $.extend({}, this.getDefaults(), this.$element.data(), options)

      if (options.delay && typeof options.delay == 'number') {
        options.delay = {
          show: options.delay,
          hide: options.delay
        }
      }

      return options
    }

    Tooltip.prototype.getDelegateOptions = function () {
      var options  = {}
      var defaults = this.getDefaults()

      this._options && $.each(this._options, function (key, value) {
        if (defaults[key] != value) options[key] = value
      })

      return options
    }

    Tooltip.prototype.enter = function (obj) {
      var self = obj instanceof this.constructor ?
        obj : $(obj.currentTarget).data('bs.' + this.type)

      if (!self) {
        self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
        $(obj.currentTarget).data('bs.' + this.type, self)
      }

      clearTimeout(self.timeout)

      self.hoverState = 'in'

      if (!self.options.delay || !self.options.delay.show) return self.show()

      self.timeout = setTimeout(function () {
        if (self.hoverState == 'in') self.show()
      }, self.options.delay.show)
    }

    Tooltip.prototype.leave = function (obj) {
      var self = obj instanceof this.constructor ?
        obj : $(obj.currentTarget).data('bs.' + this.type)

      if (!self) {
        self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
        $(obj.currentTarget).data('bs.' + this.type, self)
      }

      clearTimeout(self.timeout)

      self.hoverState = 'out'

      if (!self.options.delay || !self.options.delay.hide) return self.hide()

      self.timeout = setTimeout(function () {
        if (self.hoverState == 'out') self.hide()
      }, self.options.delay.hide)
    }

    Tooltip.prototype.show = function () {
      var e = $.Event('show.bs.' + this.type)

      if (this.hasContent() && this.enabled) {
        this.$element.trigger(e)

        var inDom = $.contains(document.documentElement, this.$element[0])
        if (e.isDefaultPrevented() || !inDom) return
        var that = this

        var $tip = this.tip()

        var tipId = this.getUID(this.type)

        this.setContent()
        $tip.attr('id', tipId)
        this.$element.attr('aria-describedby', tipId)

        if (this.options.animation) $tip.addClass('fade')

        var placement = typeof this.options.placement == 'function' ?
          this.options.placement.call(this, $tip[0], this.$element[0]) :
          this.options.placement

        var autoToken = /\s?auto?\s?/i
        var autoPlace = autoToken.test(placement)
        if (autoPlace) placement = placement.replace(autoToken, '') || 'top'

        $tip
          .detach()
          .css({ top: 0, left: 0, display: 'block' })
          .addClass(placement)
          .data('bs.' + this.type, this)

        this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)

        var pos          = this.getPosition()
        var actualWidth  = $tip[0].offsetWidth
        var actualHeight = $tip[0].offsetHeight

        if (autoPlace) {
          var orgPlacement = placement
          var $parent      = this.$element.parent()
          var parentDim    = this.getPosition($parent)

          placement = placement == 'bottom' && pos.top   + pos.height       + actualHeight - parentDim.scroll > parentDim.height ? 'top'    :
                      placement == 'top'    && pos.top   - parentDim.scroll - actualHeight < 0                                   ? 'bottom' :
                      placement == 'right'  && pos.right + actualWidth      > parentDim.width                                    ? 'left'   :
                      placement == 'left'   && pos.left  - actualWidth      < parentDim.left                                     ? 'right'  :
                      placement

          $tip
            .removeClass(orgPlacement)
            .addClass(placement)
        }

        var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)

        this.applyPlacement(calculatedOffset, placement)
        this.hoverState = null

        var complete = function () {
          that.$element.trigger('shown.bs.' + that.type)
        }

        $.support.transition && this.$tip.hasClass('fade') ?
          $tip
            .one('bsTransitionEnd', complete)
            .emulateTransitionEnd(150) :
          complete()
      }
    }

    Tooltip.prototype.applyPlacement = function (offset, placement) {
      var $tip   = this.tip()
      var width  = $tip[0].offsetWidth
      var height = $tip[0].offsetHeight

      // manually read margins because getBoundingClientRect includes difference
      var marginTop = parseInt($tip.css('margin-top'), 10)
      var marginLeft = parseInt($tip.css('margin-left'), 10)

      // we must check for NaN for ie 8/9
      if (isNaN(marginTop))  marginTop  = 0
      if (isNaN(marginLeft)) marginLeft = 0

      offset.top  = offset.top  + marginTop
      offset.left = offset.left + marginLeft

      // $.fn.offset doesn't round pixel values
      // so we use setOffset directly with our own function B-0
      $.offset.setOffset($tip[0], $.extend({
        using: function (props) {
          $tip.css({
            top: Math.round(props.top),
            left: Math.round(props.left)
          })
        }
      }, offset), 0)

      $tip.addClass('in')

      // check to see if placing tip in new offset caused the tip to resize itself
      var actualWidth  = $tip[0].offsetWidth
      var actualHeight = $tip[0].offsetHeight

      if (placement == 'top' && actualHeight != height) {
        offset.top = offset.top + height - actualHeight
      }

      var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight)

      if (delta.left) offset.left += delta.left
      else offset.top += delta.top

      var arrowDelta          = delta.left ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight
      var arrowPosition       = delta.left ? 'left'        : 'top'
      var arrowOffsetPosition = delta.left ? 'offsetWidth' : 'offsetHeight'

      $tip.offset(offset)
      this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], arrowPosition)
    }

    Tooltip.prototype.replaceArrow = function (delta, dimension, position) {
      this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + '%') : '')
    }

    Tooltip.prototype.setContent = function () {
      var $tip  = this.tip()
      var title = this.getTitle()

      $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
      $tip.removeClass('fade in top bottom left right')
    }

    Tooltip.prototype.hide = function () {
      var that = this
      var $tip = this.tip()
      var e    = $.Event('hide.bs.' + this.type)

      this.$element.removeAttr('aria-describedby')

      function complete() {
        if (that.hoverState != 'in') $tip.detach()
        that.$element.trigger('hidden.bs.' + that.type)
      }

      this.$element.trigger(e)

      if (e.isDefaultPrevented()) return

      $tip.removeClass('in')

      $.support.transition && this.$tip.hasClass('fade') ?
        $tip
          .one('bsTransitionEnd', complete)
          .emulateTransitionEnd(150) :
        complete()

      this.hoverState = null

      return this
    }

    Tooltip.prototype.fixTitle = function () {
      var $e = this.$element
      if ($e.attr('title') || typeof ($e.attr('data-original-title')) != 'string') {
        $e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
      }
    }

    Tooltip.prototype.hasContent = function () {
      return this.getTitle()
    }

    Tooltip.prototype.getPosition = function ($element) {
      $element   = $element || this.$element
      var el     = $element[0]
      var isBody = el.tagName == 'BODY'
      return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : null, {
        scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop(),
        width:  isBody ? $(window).width()  : $element.outerWidth(),
        height: isBody ? $(window).height() : $element.outerHeight()
      }, isBody ? { top: 0, left: 0 } : $element.offset())
    }

    Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
      return placement == 'bottom' ? { top: pos.top + pos.height,   left: pos.left + pos.width / 2 - actualWidth / 2  } :
             placement == 'top'    ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2  } :
             placement == 'left'   ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
          /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width   }

    }

    Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) {
      var delta = { top: 0, left: 0 }
      if (!this.$viewport) return delta

      var viewportPadding = this.options.viewport && this.options.viewport.padding || 0
      var viewportDimensions = this.getPosition(this.$viewport)

      if (/right|left/.test(placement)) {
        var topEdgeOffset    = pos.top - viewportPadding - viewportDimensions.scroll
        var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight
        if (topEdgeOffset < viewportDimensions.top) { // top overflow
          delta.top = viewportDimensions.top - topEdgeOffset
        } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow
          delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset
        }
      } else {
        var leftEdgeOffset  = pos.left - viewportPadding
        var rightEdgeOffset = pos.left + viewportPadding + actualWidth
        if (leftEdgeOffset < viewportDimensions.left) { // left overflow
          delta.left = viewportDimensions.left - leftEdgeOffset
        } else if (rightEdgeOffset > viewportDimensions.width) { // right overflow
          delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset
        }
      }

      return delta
    }

    Tooltip.prototype.getTitle = function () {
      var title
      var $e = this.$element
      var o  = this.options

      title = $e.attr('data-original-title')
        || (typeof o.title == 'function' ? o.title.call($e[0]) :  o.title)

      return title
    }

    Tooltip.prototype.getUID = function (prefix) {
      do prefix += ~~(Math.random() * 1000000)
      while (document.getElementById(prefix))
      return prefix
    }

    Tooltip.prototype.tip = function () {
      return (this.$tip = this.$tip || $(this.options.template))
    }

    Tooltip.prototype.arrow = function () {
      return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow'))
    }

    Tooltip.prototype.validate = function () {
      if (!this.$element[0].parentNode) {
        this.hide()
        this.$element = null
        this.options  = null
      }
    }

    Tooltip.prototype.enable = function () {
      this.enabled = true
    }

    Tooltip.prototype.disable = function () {
      this.enabled = false
    }

    Tooltip.prototype.toggleEnabled = function () {
      this.enabled = !this.enabled
    }

    Tooltip.prototype.toggle = function (e) {
      var self = this
      if (e) {
        self = $(e.currentTarget).data('bs.' + this.type)
        if (!self) {
          self = new this.constructor(e.currentTarget, this.getDelegateOptions())
          $(e.currentTarget).data('bs.' + this.type, self)
        }
      }

      self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
    }

    Tooltip.prototype.destroy = function () {
      clearTimeout(this.timeout)
      this.hide().$element.off('.' + this.type).removeData('bs.' + this.type)
    }

    // TOOLTIP PLUGIN DEFINITION
    // =========================

    function Plugin(option) {
      return this.each(function () {
        var $this   = $(this)
        var data    = $this.data('bs.tooltip')
        var options = typeof option == 'object' && option

        if (!data && option == 'destroy') return
        if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
        if (typeof option == 'string') data[option]()
      })
    }

    var old = $.fn.tooltip

    $.fn.tooltip             = Plugin
    $.fn.tooltip.Constructor = Tooltip

    // TOOLTIP NO CONFLICT
    // ===================

    $.fn.tooltip.noConflict = function () {
      $.fn.tooltip = old
      return this
    }

  })

}();
<commit_msg>fix for #13747<commit_after>/* ========================================================================
 * Bootstrap: tooltip.js v3.1.1
 * http://getbootstrap.com/javascript/#tooltip
 * Inspired by the original jQuery.tipsy by Jason Frame
 * ========================================================================
 * Copyright 2011-2014 Twitter, Inc.
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 * ======================================================================== */

+function () { 'use strict';

  (function (o_o) {
    typeof define  == 'function' && define.amd ? define(['jquery'], o_o) :
    typeof exports == 'object' ? o_o(require('jquery')) : o_o(jQuery)
  })(function ($) {

    // TOOLTIP PUBLIC CLASS DEFINITION
    // ===============================

    var Tooltip = function (element, options) {
      this.type       =
      this.options    =
      this.enabled    =
      this.timeout    =
      this.hoverState =
      this.$element   = null

      this.init('tooltip', element, options)
    }

    Tooltip.VERSION  = '3.1.1'

    Tooltip.DEFAULTS = {
      animation: true,
      placement: 'top',
      selector: false,
      template: '

',
      trigger: 'hover focus',
      title: '',
      delay: 0,
      html: false,
      container: false,
      viewport: {
        selector: 'body',
        padding: 0
      }
    }

    Tooltip.prototype.init = function (type, element, options) {
      this.enabled   = true
      this.type      = type
      this.$element  = $(element)
      this.options   = this.getOptions(options)
      this.$viewport = this.options.viewport && $(this.options.viewport.selector || this.options.viewport)

      var triggers = this.options.trigger.split(' ')

      for (var i = triggers.length; i--;) {
        var trigger = triggers[i]

        if (trigger == 'click') {
          this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
        } else if (trigger != 'manual') {
          var eventIn  = trigger == 'hover' ? 'mouseenter' : 'focusin'
          var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout'

          this.$element.on(eventIn  + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
          this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
        }
      }

      this.options.selector ?
        (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
        this.fixTitle()
    }

    Tooltip.prototype.getDefaults = function () {
      return Tooltip.DEFAULTS
    }

    Tooltip.prototype.getOptions = function (options) {
      options = $.extend({}, this.getDefaults(), this.$element.data(), options)

      if (options.delay && typeof options.delay == 'number') {
        options.delay = {
          show: options.delay,
          hide: options.delay
        }
      }

      return options
    }

    Tooltip.prototype.getDelegateOptions = function () {
      var options  = {}
      var defaults = this.getDefaults()

      this._options && $.each(this._options, function (key, value) {
        if (defaults[key] != value) options[key] = value
      })

      return options
    }

    Tooltip.prototype.enter = function (obj) {
      var self = obj instanceof this.constructor ?
        obj : $(obj.currentTarget).data('bs.' + this.type)

      if (!self) {
        self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
        $(obj.currentTarget).data('bs.' + this.type, self)
      }

      clearTimeout(self.timeout)

      self.hoverState = 'in'

      if (!self.options.delay || !self.options.delay.show) return self.show()

      self.timeout = setTimeout(function () {
        if (self.hoverState == 'in') self.show()
      }, self.options.delay.show)
    }

    Tooltip.prototype.leave = function (obj) {
      var self = obj instanceof this.constructor ?
        obj : $(obj.currentTarget).data('bs.' + this.type)

      if (!self) {
        self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
        $(obj.currentTarget).data('bs.' + this.type, self)
      }

      clearTimeout(self.timeout)

      self.hoverState = 'out'

      if (!self.options.delay || !self.options.delay.hide) return self.hide()

      self.timeout = setTimeout(function () {
        if (self.hoverState == 'out') self.hide()
      }, self.options.delay.hide)
    }

    Tooltip.prototype.show = function () {
      var e = $.Event('show.bs.' + this.type)

      if (this.hasContent() && this.enabled) {
        this.$element.trigger(e)

        var inDom = $.contains(document.documentElement, this.$element[0])
        if (e.isDefaultPrevented() || !inDom) return
        var that = this

        var $tip = this.tip()

        var tipId = this.getUID(this.type)

        this.setContent()
        $tip.attr('id', tipId)
        this.$element.attr('aria-describedby', tipId)

        if (this.options.animation) $tip.addClass('fade')

        var placement = typeof this.options.placement == 'function' ?
          this.options.placement.call(this, $tip[0], this.$element[0]) :
          this.options.placement

        var autoToken = /\s?auto?\s?/i
        var autoPlace = autoToken.test(placement)
        if (autoPlace) placement = placement.replace(autoToken, '') || 'top'

        $tip
          .detach()
          .css({ top: 0, left: 0, display: 'block' })
          .addClass(placement)
          .data('bs.' + this.type, this)

        this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)

        var pos          = this.getPosition()
        var actualWidth  = $tip[0].offsetWidth
        var actualHeight = $tip[0].offsetHeight

        if (autoPlace) {
          var orgPlacement = placement
          var $parent      = this.$element.parent()
          var parentDim    = this.getPosition($parent)

          placement = placement == 'bottom' && pos.top   + pos.height       + actualHeight - parentDim.scroll > parentDim.height ? 'top'    :
                      placement == 'top'    && pos.top   - parentDim.scroll - actualHeight < 0                                   ? 'bottom' :
                      placement == 'right'  && pos.right + actualWidth      > parentDim.width                                    ? 'left'   :
                      placement == 'left'   && pos.left  - actualWidth      < parentDim.left                                     ? 'right'  :
                      placement

          $tip
            .removeClass(orgPlacement)
            .addClass(placement)
        }

        var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)

        this.applyPlacement(calculatedOffset, placement)

        var complete = function () {
          that.$element.trigger('shown.bs.' + that.type)
          that.hoverState = null
        }

        $.support.transition && this.$tip.hasClass('fade') ?
          $tip
            .one('bsTransitionEnd', complete)
            .emulateTransitionEnd(150) :
          complete()
      }
    }

    Tooltip.prototype.applyPlacement = function (offset, placement) {
      var $tip   = this.tip()
      var width  = $tip[0].offsetWidth
      var height = $tip[0].offsetHeight

      // manually read margins because getBoundingClientRect includes difference
      var marginTop = parseInt($tip.css('margin-top'), 10)
      var marginLeft = parseInt($tip.css('margin-left'), 10)

      // we must check for NaN for ie 8/9
      if (isNaN(marginTop))  marginTop  = 0
      if (isNaN(marginLeft)) marginLeft = 0

      offset.top  = offset.top  + marginTop
      offset.left = offset.left + marginLeft

      // $.fn.offset doesn't round pixel values
      // so we use setOffset directly with our own function B-0
      $.offset.setOffset($tip[0], $.extend({
        using: function (props) {
          $tip.css({
            top: Math.round(props.top),
            left: Math.round(props.left)
          })
        }
      }, offset), 0)

      $tip.addClass('in')

      // check to see if placing tip in new offset caused the tip to resize itself
      var actualWidth  = $tip[0].offsetWidth
      var actualHeight = $tip[0].offsetHeight

      if (placement == 'top' && actualHeight != height) {
        offset.top = offset.top + height - actualHeight
      }

      var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight)

      if (delta.left) offset.left += delta.left
      else offset.top += delta.top

      var arrowDelta          = delta.left ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight
      var arrowPosition       = delta.left ? 'left'        : 'top'
      var arrowOffsetPosition = delta.left ? 'offsetWidth' : 'offsetHeight'

      $tip.offset(offset)
      this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], arrowPosition)
    }

    Tooltip.prototype.replaceArrow = function (delta, dimension, position) {
      this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + '%') : '')
    }

    Tooltip.prototype.setContent = function () {
      var $tip  = this.tip()
      var title = this.getTitle()

      $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
      $tip.removeClass('fade in top bottom left right')
    }

    Tooltip.prototype.hide = function () {
      var that = this
      var $tip = this.tip()
      var e    = $.Event('hide.bs.' + this.type)

      this.$element.removeAttr('aria-describedby')

      function complete() {
        if (that.hoverState != 'in') $tip.detach()
        that.$element.trigger('hidden.bs.' + that.type)
      }

      this.$element.trigger(e)

      if (e.isDefaultPrevented()) return

      $tip.removeClass('in')

      $.support.transition && this.$tip.hasClass('fade') ?
        $tip
          .one('bsTransitionEnd', complete)
          .emulateTransitionEnd(150) :
        complete()

      this.hoverState = null

      return this
    }

    Tooltip.prototype.fixTitle = function () {
      var $e = this.$element
      if ($e.attr('title') || typeof ($e.attr('data-original-title')) != 'string') {
        $e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
      }
    }

    Tooltip.prototype.hasContent = function () {
      return this.getTitle()
    }

    Tooltip.prototype.getPosition = function ($element) {
      $element   = $element || this.$element
      var el     = $element[0]
      var isBody = el.tagName == 'BODY'
      return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : null, {
        scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop(),
        width:  isBody ? $(window).width()  : $element.outerWidth(),
        height: isBody ? $(window).height() : $element.outerHeight()
      }, isBody ? { top: 0, left: 0 } : $element.offset())
    }

    Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
      return placement == 'bottom' ? { top: pos.top + pos.height,   left: pos.left + pos.width / 2 - actualWidth / 2  } :
             placement == 'top'    ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2  } :
             placement == 'left'   ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
          /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width   }

    }

    Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) {
      var delta = { top: 0, left: 0 }
      if (!this.$viewport) return delta

      var viewportPadding = this.options.viewport && this.options.viewport.padding || 0
      var viewportDimensions = this.getPosition(this.$viewport)

      if (/right|left/.test(placement)) {
        var topEdgeOffset    = pos.top - viewportPadding - viewportDimensions.scroll
        var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight
        if (topEdgeOffset < viewportDimensions.top) { // top overflow
          delta.top = viewportDimensions.top - topEdgeOffset
        } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow
          delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset
        }
      } else {
        var leftEdgeOffset  = pos.left - viewportPadding
        var rightEdgeOffset = pos.left + viewportPadding + actualWidth
        if (leftEdgeOffset < viewportDimensions.left) { // left overflow
          delta.left = viewportDimensions.left - leftEdgeOffset
        } else if (rightEdgeOffset > viewportDimensions.width) { // right overflow
          delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset
        }
      }

      return delta
    }

    Tooltip.prototype.getTitle = function () {
      var title
      var $e = this.$element
      var o  = this.options

      title = $e.attr('data-original-title')
        || (typeof o.title == 'function' ? o.title.call($e[0]) :  o.title)

      return title
    }

    Tooltip.prototype.getUID = function (prefix) {
      do prefix += ~~(Math.random() * 1000000)
      while (document.getElementById(prefix))
      return prefix
    }

    Tooltip.prototype.tip = function () {
      return (this.$tip = this.$tip || $(this.options.template))
    }

    Tooltip.prototype.arrow = function () {
      return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow'))
    }

    Tooltip.prototype.validate = function () {
      if (!this.$element[0].parentNode) {
        this.hide()
        this.$element = null
        this.options  = null
      }
    }

    Tooltip.prototype.enable = function () {
      this.enabled = true
    }

    Tooltip.prototype.disable = function () {
      this.enabled = false
    }

    Tooltip.prototype.toggleEnabled = function () {
      this.enabled = !this.enabled
    }

    Tooltip.prototype.toggle = function (e) {
      var self = this
      if (e) {
        self = $(e.currentTarget).data('bs.' + this.type)
        if (!self) {
          self = new this.constructor(e.currentTarget, this.getDelegateOptions())
          $(e.currentTarget).data('bs.' + this.type, self)
        }
      }

      self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
    }

    Tooltip.prototype.destroy = function () {
      clearTimeout(this.timeout)
      this.hide().$element.off('.' + this.type).removeData('bs.' + this.type)
    }

    // TOOLTIP PLUGIN DEFINITION
    // =========================

    function Plugin(option) {
      return this.each(function () {
        var $this   = $(this)
        var data    = $this.data('bs.tooltip')
        var options = typeof option == 'object' && option

        if (!data && option == 'destroy') return
        if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
        if (typeof option == 'string') data[option]()
      })
    }

    var old = $.fn.tooltip

    $.fn.tooltip             = Plugin
    $.fn.tooltip.Constructor = Tooltip

    // TOOLTIP NO CONFLICT
    // ===================

    $.fn.tooltip.noConflict = function () {
      $.fn.tooltip = old
      return this
    }

  })

}();
<|endoftext|>


<commit_before>(function () {
  "use strict";

  const _ = require('lodash');
  const Promise = require('bluebird');
  const fs = require('fs');
  const path = require('path');
  const assert = require('assert');

  const db = require('remote').require('./js/db');
  const models = require('remote').require('./js/models');

  // path to the gradient textures directory
  const GRADIENT_BASE = 'img/gradients';

  Vue.config.debug = true;

  Vue.component('vis-canvas', {
    template: '#visCanvas',
    props: ['heatmap', 'gradientPath'],
    ready() {
      this.heatmap = createWebGLHeatmap({
        canvas: this.$el,
        intensityToAlpha: true,
        gradientTexture: 'img/gradients/spectrum.png'
      });

      this.$watch('gradientPath', () => {
        let image = new Image();
        image.onload = () => {
          return this.heatmap.gradientTexture.bind().upload(image);
        };
        image.src = this.gradientPath;
      });
    }
  });

  Vue.component('vis-query-form', {
    template: '#queryForm',
    props: ['heatmap', 'enabled', 'gradientPath', 'gradientTextures', 'readyToVisualise'],
    replace: false,
    data: function () {
      return {
        selectedSession: null,
        selectedEvent: null,
        selectedLocation: null,
        sessions: [],
        filters: [],
        events: [],
        points: [],
        comparators: [
          {
            text: '=',
            sql: '=',
            cast: 'text'
          },
          {
            text: '<',
            sql: '<',
            cast: 'int'
          },
          {
            text: '≤',
            sql: '<=',
            cast: 'int'
          },
          {
            text: '>',
            sql: '>',
            cast: 'int'
          },
          {
            text: '≥',
            sql: '>=',
            cast: 'int'
          }
        ]
      };
    },
    computed: {
      readyToVisualise() {
        return this.selectedSession && this.selectedEvent && this.selectedLocation && this.gradientPath;
      }
    },
    ready() {
      this.$watch('selectedSession', () => {
        this.points = [];
        this.events = [];

        db.query(`SELECT DISTINCT ON (name) name, locations, entities
FROM events
WHERE events.session_id = :sessionId
AND events.locations IS NOT NULL
ORDER BY name`, {
            type: db.QueryTypes.SELECT,
            replacements: {sessionId: this.selectedSession.id}
          })
          .then(results => {
            this.events = results.map(row => {
              return {
                name: row.name,
                locations: _.keys(row.locations),
                entities: _.keys(row.entities)
              }
            });

            console.log('Got events:', this.events);
          })
          .catch(err => this.$dispatch('error', err));

        $(this.$root.$els.canvasBackdrop)
          .css('background-image', `url(overviews/${this.selectedSession.game}/${this.selectedSession.level}.png)`)
          .css('background-color', 'black');
      });

      this.$watch('points', () => {
        if (this.heatmap == null) {
          console.warn('no heatmap to update');
          return;
        }
        this.heatmap.clear();
        this.heatmap.addPoints(this.points);
      });

      this.refreshSessions();
    },
    methods: {
      refreshSessions() {
        models.Session.findAll({
            attributes: ['id', 'level', 'title', 'game'],
            order: [['id', 'DESC']]
          })
          .then(sessions => {
            this.sessions = sessions.map(x => _.toPlainObject(x.get({plain: true})));
            console.log('Got sessions:', this.sessions);
          })
          .catch(err => this.$dispatch('error', err));
      },
      addFilter() {
        this.filters.push({
          id: Math.random(),
          target: null,
          prop: '',
          comparator: null,
          value: ''
        });
      },
      removeFilter(index) {
        this.filters.splice(index, 1);
      },
      getResults(intensity) {
        let queryString = `SELECT *, (events.locations ->> :location) AS position
          FROM events
          WHERE events.name = :event
          AND events.session_id = :sessionId
          AND events.locations ? :location`;

        this.filters.forEach(filter => {
          if (filter.target === '_event') {
            queryString += `\nAND (events.data->>${db.escape(filter.prop)})::${filter.comparator.cast} ${filter.comparator.sql} ${db.escape(filter.value)}`;
          } else {
            queryString += `\nAND ((
              SELECT entity_props.value
              FROM entity_props
              WHERE entity_props.index = (events.entities->>${db.escape(filter.target)})::int
              AND entity_props.tick <= events.tick
              AND entity_props.prop = ${db.escape(filter.prop)}
              AND entity_props.session_id = events.session_id
              ORDER BY entity_props.tick DESC
              LIMIT 1
            )->>'value')::${filter.comparator.cast} ${filter.comparator.sql} ${db.escape(filter.value)}`;
          }
        });

        console.log(' *** Query');

        this.points = [];
        console.time('query');

        return db.query(queryString, {
            type: db.QueryTypes.SELECT,
            replacements: {
              event: this.selectedEvent.name,
              sessionId: this.selectedSession.id,
              location: this.selectedLocation
            }
          })
          .then(results => {
            console.timeEnd('query');

            console.log('Query returned %d rows', results.length);

            let overviewData = require(`./overviews/${this.selectedSession.game}/${this.selectedSession.level}.json`);

            console.time('render');

            this.points = results.map(row => {
              let position = JSON.parse(row.position);
              let x = (position.x - overviewData.pos_x) / overviewData.scale;
              let y = (overviewData.pos_y - position.y) / overviewData.scale;

              return {x, y, scale: overviewData.scale, intensity};
            });

            console.timeEnd('render');
          })
          .catch(err => this.$dispatch('error', err));
      },
    }
  });

  const app = new Vue({
    el: '#app',
    data: {
      queries: [],
      alerts: [],
      intensity: 0.5,
      querying: false,
      gradientTextures: []
    },
    computed: {
      readyToVisualise() {
        return this.queries.reduce((memo, q) => memo && q.readyToVisualise, true);
      }
    },
    methods: {
      onError(error) {
        this.alerts.push({
          className: 'danger',
          headline: 'Unexpected error',
          stack: error.stack
        })
      },
      dismissAlert(index) {
        this.alerts.splice(index, 1);
      },
      visualise() {
        if (!this.readyToVisualise) {
          return this.alerts.push({
            className: 'warning',
            headline: 'All inputs must be complete to visualise'
          });
        }

        this.querying = true;

        Promise.map(
          this.$refs.query,
          q => q.getResults(this.intensity)
        ).then(() => {
          this.querying = false;
        });
      },
      addQuery() {
        let i = this.queries.push({
          id: Math.random(),
          selected: false,
          heatmap: null,
          gradientPath: null,
          readyToVisualise: false
        });

        this.switchQueryTab(i - 1);
      },
      closeQuery(index) {
        console.log('closeQuery', index);

        // find index of the selected tab
        let selectedIndex = this.queries.findIndex(q => q.selected);

        this.queries.splice(index, 1);

        let closedSelected = index == selectedIndex;

        // no choice if there's no tabs remaining or only 1 left
        if (this.queries.length === 0) {
          return this.addQuery();
        }
        if (this.queries.length === 1) {
          return this.switchQueryTab(0);
        }

        // if the tab was selected upon closing, switch to the tab to the left
        if (closedSelected) {
          return this.switchQueryTab(Math.min(index, this.queries.length - 1));
        }

        if (index > selectedIndex) {
          return this.switchQueryTab(index - 1);
        }
      },
      switchQueryTab(index) {
        console.log('switchQueryTab', index);
        this.queries.forEach((q, i) => {
          q.selected = i == index
        });
      },
      tick() {
        this.queries.filter(q => q.heatmap)
          .forEach(q => {
            q.heatmap.update();
            q.heatmap.display();
          });

        window.requestAnimationFrame(this.tick.bind(this));
      },
      updateGradients(err, files) {
        assert.ifError(err);
        this.gradientTextures = files.filter(name => !name.startsWith('.'))
          .map(file => {
            return {
              path: path.join(GRADIENT_BASE, file),
              baseName: path.parse(file).name
            }
          });
      }
    },
    ready() {
      this.addQuery();
      this.tick();

      this.$on('error', err => this.onError(err));

      fs.readdir(GRADIENT_BASE, this.updateGradients.bind(this));
    }
  });

  window.app = app;
})();
<commit_msg>:sparkles: Added ≠ comparator<commit_after>(function () {
  "use strict";

  const _ = require('lodash');
  const Promise = require('bluebird');
  const fs = require('fs');
  const path = require('path');
  const assert = require('assert');

  const db = require('remote').require('./js/db');
  const models = require('remote').require('./js/models');

  // path to the gradient textures directory
  const GRADIENT_BASE = 'img/gradients';

  Vue.config.debug = true;

  Vue.component('vis-canvas', {
    template: '#visCanvas',
    props: ['heatmap', 'gradientPath'],
    ready() {
      this.heatmap = createWebGLHeatmap({
        canvas: this.$el,
        intensityToAlpha: true,
        gradientTexture: 'img/gradients/spectrum.png'
      });

      this.$watch('gradientPath', () => {
        let image = new Image();
        image.onload = () => {
          return this.heatmap.gradientTexture.bind().upload(image);
        };
        image.src = this.gradientPath;
      });
    }
  });

  Vue.component('vis-query-form', {
    template: '#queryForm',
    props: ['heatmap', 'enabled', 'gradientPath', 'gradientTextures', 'readyToVisualise'],
    replace: false,
    data: function () {
      return {
        selectedSession: null,
        selectedEvent: null,
        selectedLocation: null,
        sessions: [],
        filters: [],
        events: [],
        points: [],
        comparators: [
          {
            text: '=',
            sql: '=',
            cast: 'text'
          },
          {
            text: '≠',
            sql: '!=',
            cast: 'text'
          },
          {
            text: '<',
            sql: '<',
            cast: 'int'
          },
          {
            text: '≤',
            sql: '<=',
            cast: 'int'
          },
          {
            text: '>',
            sql: '>',
            cast: 'int'
          },
          {
            text: '≥',
            sql: '>=',
            cast: 'int'
          }
        ]
      };
    },
    computed: {
      readyToVisualise() {
        return this.selectedSession && this.selectedEvent && this.selectedLocation && this.gradientPath;
      }
    },
    ready() {
      this.$watch('selectedSession', () => {
        this.points = [];
        this.events = [];

        db.query(`SELECT DISTINCT ON (name) name, locations, entities
FROM events
WHERE events.session_id = :sessionId
AND events.locations IS NOT NULL
ORDER BY name`, {
            type: db.QueryTypes.SELECT,
            replacements: {sessionId: this.selectedSession.id}
          })
          .then(results => {
            this.events = results.map(row => {
              return {
                name: row.name,
                locations: _.keys(row.locations),
                entities: _.keys(row.entities)
              }
            });

            console.log('Got events:', this.events);
          })
          .catch(err => this.$dispatch('error', err));

        $(this.$root.$els.canvasBackdrop)
          .css('background-image', `url(overviews/${this.selectedSession.game}/${this.selectedSession.level}.png)`)
          .css('background-color', 'black');
      });

      this.$watch('points', () => {
        if (this.heatmap == null) {
          console.warn('no heatmap to update');
          return;
        }
        this.heatmap.clear();
        this.heatmap.addPoints(this.points);
      });

      this.refreshSessions();
    },
    methods: {
      refreshSessions() {
        models.Session.findAll({
            attributes: ['id', 'level', 'title', 'game'],
            order: [['id', 'DESC']]
          })
          .then(sessions => {
            this.sessions = sessions.map(x => _.toPlainObject(x.get({plain: true})));
            console.log('Got sessions:', this.sessions);
          })
          .catch(err => this.$dispatch('error', err));
      },
      addFilter() {
        this.filters.push({
          id: Math.random(),
          target: null,
          prop: '',
          comparator: null,
          value: ''
        });
      },
      removeFilter(index) {
        this.filters.splice(index, 1);
      },
      getResults(intensity) {
        let queryString = `SELECT *, (events.locations ->> :location) AS position
          FROM events
          WHERE events.name = :event
          AND events.session_id = :sessionId
          AND events.locations ? :location`;

        this.filters.forEach(filter => {
          if (filter.target === '_event') {
            queryString += `\nAND (events.data->>${db.escape(filter.prop)})::${filter.comparator.cast} ${filter.comparator.sql} ${db.escape(filter.value)}`;
          } else {
            queryString += `\nAND ((
              SELECT entity_props.value
              FROM entity_props
              WHERE entity_props.index = (events.entities->>${db.escape(filter.target)})::int
              AND entity_props.tick <= events.tick
              AND entity_props.prop = ${db.escape(filter.prop)}
              AND entity_props.session_id = events.session_id
              ORDER BY entity_props.tick DESC
              LIMIT 1
            )->>'value')::${filter.comparator.cast} ${filter.comparator.sql} ${db.escape(filter.value)}`;
          }
        });

        console.log(' *** Query');

        this.points = [];
        console.time('query');

        return db.query(queryString, {
            type: db.QueryTypes.SELECT,
            replacements: {
              event: this.selectedEvent.name,
              sessionId: this.selectedSession.id,
              location: this.selectedLocation
            }
          })
          .then(results => {
            console.timeEnd('query');

            console.log('Query returned %d rows', results.length);

            let overviewData = require(`./overviews/${this.selectedSession.game}/${this.selectedSession.level}.json`);

            console.time('render');

            this.points = results.map(row => {
              let position = JSON.parse(row.position);
              let x = (position.x - overviewData.pos_x) / overviewData.scale;
              let y = (overviewData.pos_y - position.y) / overviewData.scale;

              return {x, y, scale: overviewData.scale, intensity};
            });

            console.timeEnd('render');
          })
          .catch(err => this.$dispatch('error', err));
      },
    }
  });

  const app = new Vue({
    el: '#app',
    data: {
      queries: [],
      alerts: [],
      intensity: 0.5,
      querying: false,
      gradientTextures: []
    },
    computed: {
      readyToVisualise() {
        return this.queries.reduce((memo, q) => memo && q.readyToVisualise, true);
      }
    },
    methods: {
      onError(error) {
        this.alerts.push({
          className: 'danger',
          headline: 'Unexpected error',
          stack: error.stack
        })
      },
      dismissAlert(index) {
        this.alerts.splice(index, 1);
      },
      visualise() {
        if (!this.readyToVisualise) {
          return this.alerts.push({
            className: 'warning',
            headline: 'All inputs must be complete to visualise'
          });
        }

        this.querying = true;

        Promise.map(
          this.$refs.query,
          q => q.getResults(this.intensity)
        ).then(() => {
          this.querying = false;
        });
      },
      addQuery() {
        let i = this.queries.push({
          id: Math.random(),
          selected: false,
          heatmap: null,
          gradientPath: null,
          readyToVisualise: false
        });

        this.switchQueryTab(i - 1);
      },
      closeQuery(index) {
        console.log('closeQuery', index);

        // find index of the selected tab
        let selectedIndex = this.queries.findIndex(q => q.selected);

        this.queries.splice(index, 1);

        let closedSelected = index == selectedIndex;

        // no choice if there's no tabs remaining or only 1 left
        if (this.queries.length === 0) {
          return this.addQuery();
        }
        if (this.queries.length === 1) {
          return this.switchQueryTab(0);
        }

        // if the tab was selected upon closing, switch to the tab to the left
        if (closedSelected) {
          return this.switchQueryTab(Math.min(index, this.queries.length - 1));
        }

        if (index > selectedIndex) {
          return this.switchQueryTab(index - 1);
        }
      },
      switchQueryTab(index) {
        console.log('switchQueryTab', index);
        this.queries.forEach((q, i) => {
          q.selected = i == index
        });
      },
      tick() {
        this.queries.filter(q => q.heatmap)
          .forEach(q => {
            q.heatmap.update();
            q.heatmap.display();
          });

        window.requestAnimationFrame(this.tick.bind(this));
      },
      updateGradients(err, files) {
        assert.ifError(err);
        this.gradientTextures = files.filter(name => !name.startsWith('.'))
          .map(file => {
            return {
              path: path.join(GRADIENT_BASE, file),
              baseName: path.parse(file).name
            }
          });
      }
    },
    ready() {
      this.addQuery();
      this.tick();

      this.$on('error', err => this.onError(err));

      fs.readdir(GRADIENT_BASE, this.updateGradients.bind(this));
    }
  });

  window.app = app;
})();
<|endoftext|>


<commit_before>function TempoEvent(type, item, element) {
    this.type = type;
    this.item = item;
    this.element = element;

    return this;
}

TempoEvent.Types = {
    RENDER_STARTING : 'render_starting',
    ITEM_RENDER_STARTING : 'item_render_starting',
    ITEM_RENDER_COMPLETE : 'item_render_complete',
    RENDER_COMPLETE : 'render_complete'
};

var Tempo = (function (tempo) {

    /*!
     * Helpers
     */
    var utils = {
        startsWith : function (str, prefix) {
            return (str.indexOf(prefix) === 0);
        },

        replaceVariable : function (item, str) {
            return str.replace(/\{\{([A-Za-z0-9\._\[\]]*?)\}\}/g, function (match, variable) {
				var val = null;
				if (utils.typeOf(item) === 'array') {
					val = eval('item' + variable);
				} else {
					val = eval('item.' + variable);
				}
                if (val !== undefined) {
                    return val;
                }
                return '';
            });
        },

        clearContainer : function (el) {
            if (el !== undefined && el.childNodes !== undefined) {
                for (var i = el.childNodes.length; i >= 0; i--) {
                    if (el.childNodes[i] !== undefined && el.childNodes[i].getAttribute !== undefined && el.childNodes[i].getAttribute('data-template') !== null) {
                        el.childNodes[i].parentNode.removeChild(el.childNodes[i]);
                    }
                }
            }
        },

        isNested : function (el) {
            var p = el.parentNode;
            while (p) {
                if (p.getAttribute !== undefined && p.getAttribute('data-template') !== null) {
                    return true;
                }
                p = p.parentNode;
            }
            return false;
        },

        equalsIgnoreCase : function (str1, str2) {
            return str1.toLowerCase() === str2.toLowerCase();
        },

        getElement : function (template, html) {
            if (utils.equalsIgnoreCase(template.tagName, 'tr')) {
                // Wrapping to get around read-only innerHTML
                var el = document.createElement('div');
                el.innerHTML = '

' + html + '

';
                var depth = 3;
                while (depth--) {
                    el = el.lastChild;
                }
                return el;
            } else {
                // No need to wrap
                template.innerHTML = html;
                return template;
            }
        },

        typeOf : function (obj) {
            if (typeof(obj) === "object") {
                if (obj === null) {
                    return "null";
                }
                if (obj.constructor === ([]).constructor) {
                    return "array";
                }
                if (obj.constructor === (new Date()).constructor) {
                    return "date";
                }
                if (obj.constructor === (new RegExp()).constructor) {
                    return "regex";
                }
                return "object";
            }
            return typeof(obj);
        },

        notify : function (listener, event) {
            if (listener !== undefined) {
                listener(event);
            }
        }
    };

    function Templates(nestedItem) {
        this.defaultTemplate = null;
        this.namedTemplates = {};
        this.container = null;
		this.nestedItem = nestedItem !== undefined ? nestedItem : null;

        return this;
    }

    Templates.prototype = {
        parse: function (container) {
            this.container = container;
            var children = container.getElementsByTagName('*');

            for (var i = 0; i < children.length; i++) {
                if (children[i].getAttribute('data-template') !== null && (this.nestedItem === children[i].getAttribute('data-template') || children[i].getAttribute('data-template') === '' && !utils.isNested(children[i]))) {
                    this.createTemplate(children[i]);
                }
            }

			// If there is no default template (data-template) then create one from container
			if (this.defaultTemplate === null) {
				// Creating a template inside the container
				var el = document.createElement('div');
				el.setAttribute('data-template', '');
				el.innerHTML = this.container.innerHTML;

				// Clearing container before adding the wrapped contents
				this.container.innerHTML = '';

				// There is now a default template present with a data-template attribute
				this.container.appendChild(el);
				this.createTemplate(el);
			}

            utils.clearContainer(this.container);
        },

        createTemplate : function (node) {
            var element = node.cloneNode(true);

            // Clear display: none;
            if (element.style.removeAttribute) {
                element.style.removeAttribute("display");
            }
            else {
                element.style.removeProperty("display");
            }

            // Remapping container element in case template
            // is deep in container
			this.container = node.parentNode;

            // Element is a template
			var nonDefault = false;
            for (var a = 0; a < element.attributes.length; a++) {
                var attr = element.attributes[a];
                // If attribute
                if (utils.startsWith(attr.name, 'data-if-')) {
                    var val;
                    if (attr.value === '') {
                        val = true;
                    } else {
                        val = '\'' + attr.value + '\'';
                    }
                    this.namedTemplates[attr.name.substring(8, attr.name.length) + '==' + val] = element;
                    element.removeAttribute(attr.name);
					nonDefault = true;
                }
            }

            // Setting as default template, last one wins
            if (!nonDefault) {
				this.defaultTemplate = element;
			}
        },

        templateFor: function (item) {
            for (var templateName in this.namedTemplates) {
                if (eval('item.' + templateName)) {
                    return this.namedTemplates[templateName].cloneNode(true);
                }
            }
            if (this.defaultTemplate) {
                return this.defaultTemplate.cloneNode(true);
            }
        }
    };

    /*!
     * Renderer for populating containers with data using templates.
     */
    function Renderer(templates) {
        this.templates = templates;
        this.listener = undefined;
        this.started = false;

        return this;
    }

    Renderer.prototype = {
        notify : function (listener) {
            this.listener = listener;

            return this;
        },

        starting : function () {
            // Use this to manually fire the RENDER_STARTING event e.g. just before you issue an AJAX request
            // Useful if you're not calling prepare immediately before render
            this.started = true;
            utils.notify(this.listener, new TempoEvent(TempoEvent.Types.RENDER_STARTING, undefined, undefined));

            return this;
        },

        render : function (data) {
            // Check if starting event was manually fired
            if (!this.started) {
                utils.notify(this.listener, new TempoEvent(TempoEvent.Types.RENDER_STARTING, undefined, undefined));
            }

            utils.clearContainer(this.templates.container);

            if (data) {
                // If object then wrapping in an array
                if (utils.typeOf(data) === 'object') {
                    data = [data];
                }

                var fragment = document.createDocumentFragment();

                for (var i = 0; i < data.length; i++) {
                    this.renderItem(this, data[i], fragment);
                }

                this.templates.container.appendChild(fragment);
            }

            utils.notify(this.listener, new TempoEvent(TempoEvent.Types.RENDER_COMPLETE, undefined, undefined));

            return this;
        },

        renderItem : function (renderer, item, fragment) {
            var template = renderer.templates.templateFor(item);
            if (template && item) {
                utils.notify(this.listener, new TempoEvent(TempoEvent.Types.ITEM_RENDER_STARTING, item, template));

                var nestedDeclaration = template.innerHTML.match(/data-template="(.*?)"/g);
				if (nestedDeclaration) {
					for (var i = 0; i < nestedDeclaration.length; i++) {
						var nested = nestedDeclaration[i].match(/"(.*?)"/)[1];

						var t = new Templates(nested);
	                    t.parse(template);

	                    var r = new Renderer(t);
	                    r.render(eval('item.' + nested));
					}
				}

                // Dealing with HTML as a String from now on (to be reviewed)
				// Attribute values are escaped in FireFox so making sure there are no escaped tags
                var html = template.innerHTML.replace(/%7B%7B/g, '{{').replace(/%7D%7D/g, '}}');

                // Tags
                for (var p in renderer.tags) {
					alert(renderer.tags[p].regex)
                    html = html.replace(new RegExp(renderer.tags[p].regex, 'gi'), renderer.tags[p].handler(renderer, item));
                }

                // Content
                html = utils.replaceVariable(item, html);

                // Template class attribute
                if (template.getAttribute('class')) {
                    template.className = utils.replaceVariable(item, template.className);
                }

                // Template id
                if (template.getAttribute('id')) {
                    template.id = utils.replaceVariable(item, template.id);
                }

                fragment.appendChild(utils.getElement(template, html));

                utils.notify(this.listener, new TempoEvent(TempoEvent.Types.ITEM_RENDER_COMPLETE, item, template));
            }
        },

        tags : [
            // If tag
            {'regex': '\\{\\{if (.*?)\\}\\}(.*?)\\{\\{endif\\}\\}', 'handler': function (renderer, item) {
                return function (match, condition, content) {
                    var member_regex = '';
                    for (var member in item) {
                        if (member_regex.length > 0) {
                            member_regex += '|';
                        }
                        member_regex += member;
                    }

                    condition = condition.replace(/&/g, '&');
                    condition = condition.replace(new RegExp(member_regex, 'gi'), function (match) {
                        return 'item.' + match;
                    });
					alert(condition);
                    if (eval(condition)) {
                        return content;
                    }

                    return '';
                };
            }
            }
        ]
    };

    /*!
     * Prepare a container for rendering, gathering templates and
     * clearing afterwards.
     */
    tempo.prepare = function (container) {
        if (typeof container === 'string') {
            container = document.getElementById(container);
        }

        var templates = new Templates(false);
        templates.parse(container);

        return new Renderer(templates);
    };

    return tempo;

})(Tempo || {});
<commit_msg>testing<commit_after>function TempoEvent(type, item, element) {
    this.type = type;
    this.item = item;
    this.element = element;

    return this;
}

TempoEvent.Types = {
    RENDER_STARTING : 'render_starting',
    ITEM_RENDER_STARTING : 'item_render_starting',
    ITEM_RENDER_COMPLETE : 'item_render_complete',
    RENDER_COMPLETE : 'render_complete'
};

var Tempo = (function (tempo) {

    /*!
     * Helpers
     */
    var utils = {
        startsWith : function (str, prefix) {
            return (str.indexOf(prefix) === 0);
        },

        replaceVariable : function (item, str) {
            return str.replace(/\{\{([A-Za-z0-9\._\[\]]*?)\}\}/g, function (match, variable) {
				var val = null;
				if (utils.typeOf(item) === 'array') {
					val = eval('item' + variable);
				} else {
					val = eval('item.' + variable);
				}
                if (val !== undefined) {
                    return val;
                }
                return '';
            });
        },

        clearContainer : function (el) {
            if (el !== undefined && el.childNodes !== undefined) {
                for (var i = el.childNodes.length; i >= 0; i--) {
                    if (el.childNodes[i] !== undefined && el.childNodes[i].getAttribute !== undefined && el.childNodes[i].getAttribute('data-template') !== null) {
                        el.childNodes[i].parentNode.removeChild(el.childNodes[i]);
                    }
                }
            }
        },

        isNested : function (el) {
            var p = el.parentNode;
            while (p) {
                if (p.getAttribute !== undefined && p.getAttribute('data-template') !== null) {
                    return true;
                }
                p = p.parentNode;
            }
            return false;
        },

        equalsIgnoreCase : function (str1, str2) {
            return str1.toLowerCase() === str2.toLowerCase();
        },

        getElement : function (template, html) {
            if (utils.equalsIgnoreCase(template.tagName, 'tr')) {
                // Wrapping to get around read-only innerHTML
                var el = document.createElement('div');
                el.innerHTML = '

' + html + '

';
                var depth = 3;
                while (depth--) {
                    el = el.lastChild;
                }
                return el;
            } else {
                // No need to wrap
                template.innerHTML = html;
                return template;
            }
        },

        typeOf : function (obj) {
            if (typeof(obj) === "object") {
                if (obj === null) {
                    return "null";
                }
                if (obj.constructor === ([]).constructor) {
                    return "array";
                }
                if (obj.constructor === (new Date()).constructor) {
                    return "date";
                }
                if (obj.constructor === (new RegExp()).constructor) {
                    return "regex";
                }
                return "object";
            }
            return typeof(obj);
        },

        notify : function (listener, event) {
            if (listener !== undefined) {
                listener(event);
            }
        }
    };

    function Templates(nestedItem) {
        this.defaultTemplate = null;
        this.namedTemplates = {};
        this.container = null;
		this.nestedItem = nestedItem !== undefined ? nestedItem : null;

        return this;
    }

    Templates.prototype = {
        parse: function (container) {
            this.container = container;
            var children = container.getElementsByTagName('*');

            for (var i = 0; i < children.length; i++) {
                if (children[i].getAttribute('data-template') !== null && (this.nestedItem === children[i].getAttribute('data-template') || children[i].getAttribute('data-template') === '' && !utils.isNested(children[i]))) {
                    this.createTemplate(children[i]);
                }
            }

			// If there is no default template (data-template) then create one from container
			if (this.defaultTemplate === null) {
				// Creating a template inside the container
				var el = document.createElement('div');
				el.setAttribute('data-template', '');
				el.innerHTML = this.container.innerHTML;

				// Clearing container before adding the wrapped contents
				this.container.innerHTML = '';

				// There is now a default template present with a data-template attribute
				this.container.appendChild(el);
				this.createTemplate(el);
			}

            utils.clearContainer(this.container);
        },

        createTemplate : function (node) {
            var element = node.cloneNode(true);

            // Clear display: none;
            if (element.style.removeAttribute) {
                element.style.removeAttribute("display");
            }
            else {
                element.style.removeProperty("display");
            }

            // Remapping container element in case template
            // is deep in container
			this.container = node.parentNode;

            // Element is a template
			var nonDefault = false;
            for (var a = 0; a < element.attributes.length; a++) {
                var attr = element.attributes[a];
                // If attribute
                if (utils.startsWith(attr.name, 'data-if-')) {
                    var val;
                    if (attr.value === '') {
                        val = true;
                    } else {
                        val = '\'' + attr.value + '\'';
                    }
                    this.namedTemplates[attr.name.substring(8, attr.name.length) + '==' + val] = element;
                    element.removeAttribute(attr.name);
					nonDefault = true;
                }
            }

            // Setting as default template, last one wins
            if (!nonDefault) {
				this.defaultTemplate = element;
			}
        },

        templateFor: function (item) {
            for (var templateName in this.namedTemplates) {
                if (eval('item.' + templateName)) {
                    return this.namedTemplates[templateName].cloneNode(true);
                }
            }
            if (this.defaultTemplate) {
                return this.defaultTemplate.cloneNode(true);
            }
        }
    };

    /*!
     * Renderer for populating containers with data using templates.
     */
    function Renderer(templates) {
        this.templates = templates;
        this.listener = undefined;
        this.started = false;

        return this;
    }

    Renderer.prototype = {
        notify : function (listener) {
            this.listener = listener;

            return this;
        },

        starting : function () {
            // Use this to manually fire the RENDER_STARTING event e.g. just before you issue an AJAX request
            // Useful if you're not calling prepare immediately before render
            this.started = true;
            utils.notify(this.listener, new TempoEvent(TempoEvent.Types.RENDER_STARTING, undefined, undefined));

            return this;
        },

        render : function (data) {
            // Check if starting event was manually fired
            if (!this.started) {
                utils.notify(this.listener, new TempoEvent(TempoEvent.Types.RENDER_STARTING, undefined, undefined));
            }

            utils.clearContainer(this.templates.container);

            if (data) {
                // If object then wrapping in an array
                if (utils.typeOf(data) === 'object') {
                    data = [data];
                }

                var fragment = document.createDocumentFragment();

                for (var i = 0; i < data.length; i++) {
                    this.renderItem(this, data[i], fragment);
                }

                this.templates.container.appendChild(fragment);
            }

            utils.notify(this.listener, new TempoEvent(TempoEvent.Types.RENDER_COMPLETE, undefined, undefined));

            return this;
        },

        renderItem : function (renderer, item, fragment) {
            var template = renderer.templates.templateFor(item);
            if (template && item) {
                utils.notify(this.listener, new TempoEvent(TempoEvent.Types.ITEM_RENDER_STARTING, item, template));

                var nestedDeclaration = template.innerHTML.match(/data-template="(.*?)"/g);
				if (nestedDeclaration) {
					for (var i = 0; i < nestedDeclaration.length; i++) {
						var nested = nestedDeclaration[i].match(/"(.*?)"/)[1];

						var t = new Templates(nested);
	                    t.parse(template);

	                    var r = new Renderer(t);
	                    r.render(eval('item.' + nested));
					}
				}

                // Dealing with HTML as a String from now on (to be reviewed)
				// Attribute values are escaped in FireFox so making sure there are no escaped tags
                var html = template.innerHTML.replace(/%7B%7B/g, '{{').replace(/%7D%7D/g, '}}');

                // Tags
                for (var p in renderer.tags) {
					alert(renderer.tags[p].regex)
                    html = html.replace(new RegExp(renderer.tags[p].regex, 'gi'), renderer.tags[p].handler(renderer, item));
                }

                // Content
                html = utils.replaceVariable(item, html);

                // Template class attribute
                if (template.getAttribute('class')) {
                    template.className = utils.replaceVariable(item, template.className);
                }

                // Template id
                if (template.getAttribute('id')) {
                    template.id = utils.replaceVariable(item, template.id);
                }

                fragment.appendChild(utils.getElement(template, html));

                utils.notify(this.listener, new TempoEvent(TempoEvent.Types.ITEM_RENDER_COMPLETE, item, template));
            }
        },

        tags : [
            // If tag
            {'regex': '\\{\\{if (.*?)\\}\\}(.*?)\\{\\{endif\\}\\}', 'handler': function (renderer, item) {
                return function (match, condition, content) {
                    var member_regex = '';
                    for (var member in item) {
                        if (member_regex.length > 0) {
                            member_regex += '|';
                        }
                        member_regex += member;
                    }

                    condition = condition.replace(/&/g, '&');
					alert(condition);
                    condition = condition.replace(new RegExp(member_regex, 'gi'), function (match) {
                        return 'item.' + match;
                    });

                    if (eval(condition)) {
                        return content;
                    }

                    return '';
                };
            }
            }
        ]
    };

    /*!
     * Prepare a container for rendering, gathering templates and
     * clearing afterwards.
     */
    tempo.prepare = function (container) {
        if (typeof container === 'string') {
            container = document.getElementById(container);
        }

        var templates = new Templates(false);
        templates.parse(container);

        return new Renderer(templates);
    };

    return tempo;

})(Tempo || {});
<|endoftext|>