imdone
Version:
A task board and wiki in one!
1,567 lines (1,373 loc) • 62.1 kB
JavaScript
define([
'underscore',
'jquery',
'backbone',
'handlebars',
'json2',
'socketio',
'marked',
'prism',
'store',
'search',
'client',
'zeroclipboard',
'ace',
'tour',
'keen',
'ace-language-tools',
'ace-spellcheck',
'jqueryui',
'bootstrap',
'printThis',
'pnotify',
'hotkeys',
'toc',
'scrollTo',
'wiggle'
], function(_, $, Backbone, Handlebars, JSON, io, marked, Prism, store, Search, client, ZeroClipboard, ace, Tour, Keen) {
var imdone = window.imdone = {
data:{},
board: $("#board"),
listsMenu: $("#lists-menu"),
projectsMenu: $("#projects-dropdown"),
editorEl: $("#editor"),
editor: ace.edit("editor"),
editBar: $(".edit-bar"),
boardBar: $(".board-bar"),
fileContainer: $("#file-container"),
preview: $("#preview"),
previewContainer:$("#preview-container"),
editBtn: $("#edit-btn"),
previewToggle: $("#preview-toggle"),
previewBtn: $("#preview-btn"),
printBtn: $("#print-btn"),
filterField: $("#filter-field"),
searchDialog: $("#search-dialog"),
searchBtn: $("#search-dialog-btn"),
searchForm: $("#search-form"),
searchField: $("#search-field"),
searchResults: $("#search-results"),
searchResultsBtn:$("#search-results-btn"),
filename: $('#filename'),
fileField: $('#file-field'),
fileOpenBtn: $('#file-open'),
contentNav: $("#content-nav"),
closeFileBtn: $('#close-file-btn'),
removeFileModal: $('#remove-file-modal').modal({show:false}),
removeFileBtn: $('#remove-file-btn'),
removeFileOkBtn: $('#remove-file-ok-btn'),
removeFileName: $('#remove-file-name'),
closeFileModal: $('#close-file-modal').modal({show:false, keyboard:false}),
closeFileOkBtn: $('#close-file-ok-btn'),
closeFileCancelBtn: $('#close-file-cancel-btn'),
reloadFileModal: $('#reload-file-modal').modal({show:false, keyboard:false}),
reloadFileOkBtn: $('#reload-file-ok-btn'),
reloadFileCancelBtn: $('#reload-file-cancel-btn'),
nameFld: $('#list-name-field'),
nameModal: $('#list-name-modal'),
newListField: $('#new-list-field'),
newListModal: $('#new-list-modal'),
newListSave: $('#new-list-save'),
openReadmeBtn: $("#open-readme-btn"),
archiveBtn: $("#archive-btn"),
filterBtn: $("#filter-btn"),
closeProjectBtn: $('#close-project-btn'),
openProjectBtn: $('#project-open'),
addProjectBtn: $('#open-project-btn'),
projectNav: $('.project-nav'),
showHidden: false,
modes : {
"md":"markdown",
"markdown":"markdown",
"js":"javascript",
"javascript": "javascript",
"html":"html",
"css":"css",
"java":"java",
"json":"json",
"coffee":"coffee",
"coffeescript":"coffee",
"joe":"coffee",
"php":"php",
"py":"python",
"python":"python",
"txt":"text",
"text":"text"
},
Search: Search,
copyButton: '<button class="btn btn-inverse pull-right copy-btn" title="Copy text"><i class="icomoon-copy"></i></button>',
wiggleOpts: {
randomStart:false,
limit:10
},
pathSep: (navigator.appVersion.indexOf("Win")!=-1) ? "\\" : "/"
};
// DOING:0 Show a modal on startup that advertises chrome app and gives a poll
// DONE:30 Use [spin.js](http://fgnass.github.io/spin.js/#?lines=15&length=24&width=9&radius=60&corners=0.1&rotate=0&trail=60&speed=0.5&direction=1&hwaccel=on) for loading gif
//pnotify options
$.extend($.pnotify.defaults,{
styling: 'bootstrap',
history: false,
addclass: 'stack-bottomright',
stack: {"dir1": "up", "dir2": "left", "firstpos1": 25, "firstpos2": 25}
//stack: {"dir1": "down", "dir2": "left", "push": "bottom", "firstpos1": 45, "spacing1": 25, "spacing2": 25}
});
// Regex selector for filter
jQuery.expr[':'].regex = function(elem, index, match) {
var matchParams = match[3].split(','),
validLabels = /^(data|css):/,
attr = {
method: matchParams[0].match(validLabels) ?
matchParams[0].split(':')[0] : 'attr',
property: matchParams.shift().replace(validLabels,'')
},
regexFlags = 'ig',
regex = new RegExp(matchParams.join('').replace(/^\s+|\s+$/g,''), regexFlags);
return regex.test(jQuery(elem)[attr.method](attr.property));
};
// marked options
marked.setOptions({
gfm: true,
tables: true,
breaks: false,
pedantic: false,
sanitize: true,
smartLists: true,
langPrefix: 'language-',
});
// ZeroClipboard options
ZeroClipboard.config({ moviePath: "/lib/zeroclipboard/swf/ZeroClipboard.swf" });
// Handlebars helpers
Handlebars.registerHelper('markDown', function(md) {
return imdone.md(md);
});
Handlebars.registerHelper('concat', function(data, len) {
if (data.length > len) return data.substring(0,len-3)+"...";
return data;
});
imdone.lsTemplate = Handlebars.compile($("#files-template").html());
imdone.dirsTemplate = Handlebars.compile($("#dirs-template").html());
// #TODO:60 Replace format with _.template
String.prototype.format = function (col) {
col = typeof col === 'object' ? col : Array.prototype.slice.call(arguments, 1);
return this.replace(/\{\{|\}\}|\{(\w+)\}/g, function (m, n) {
if (m == "{{") { return "{"; }
if (m == "}}") { return "}"; }
return col[n];
});
};
String.prototype.tokenize = function() {
var args = arguments;
var result = this;
if (args.length > 0) {
for(var i=0; i<args.length; i++) {
result = result.replace(/\{\}/, args[i]);
}
}
return result;
};
// Convert markdown to html **This could be sourced from the server to DRY it up**
imdone.md = function(md) {
md = md || imdone.source.src;
// Find all code blocks and inline code in md files and save the start and end so we can ignore
var ignore = [];
var re = /`[\s\S]*?`/g, result;
while ((result = re.exec(md)) !== null) {
ignore.push([result.index, re.lastIndex]);
}
re = /`{3}[\s\S]*?`{3}/gm, result = null;
while ((result = re.exec(md)) !== null) {
ignore.push([result.index, re.lastIndex]);
}
// Replace hash style tasks but ignore code
md = md.replace(/#([\w\-]+?):(\d+?\.?\d*?)\s+(.*)/g, function(md, list, order, text, pos) {
if ( _.some(ignore, function(pair) { return ((pair[0] < pos) && (pos < pair[1])); }) ) return md;
order = (order === undefined) ? "0" : order;
return "[{0}](#{1}:{2})".format([text, list, order]);
});
var html = marked(md);
// #TODO:40 everything above this should be in imdone-core Repository or File
var links = /(<a.*?href=")(.*?)(".*?)>(.*)(<\/a>)/ig,
externalLinks = /^http/,
mailtoLinks = /^mailto/,
taskLinks = /^#([\w\-]+?):(\d+?\.{0,1}\d*?)/,
filterLinks = /^#filter\//,
inPageLinks = /^#.*$/,
gollumLinks = /(\[\[)(.*?)(\]\])/ig;
// Replace any script elements
html = html.replace(/<script.*?>([\s\S]*?)<\/.*?script>/ig,"$1").replace(/(href=["|'].*)javascript:.*(["|'].?>)/ig,"$1#$2");
// Make all links with http open in new tab
// ARCHIVE:830 For markdown files, find tasks links and give them a badge
// ARCHIVE:360 For internal inks, take them to the page
var replaceLinks = function(anchor, head, href, tail, content, end) {
// For links within links
if (new RegExp(links).test(content)) content = content.replace(links, replaceLinks);
var out = html;
// Check for external links
if (new RegExp(externalLinks).test(href)) {
out = head + href + tail + ' target="_blank">' + content + end;
// Check for task links
} else if (new RegExp(taskLinks).test(href)) {
var list;
href.replace(new RegExp(taskLinks), function(href, taskList, order) {
list = taskList;
out = href;
});
var template = '{1}{2}{3} class="task-link" data-list="{0}"> <span class="task-content">{4}</span>' +
'<span class="label label-info task-label">{0}</span>{5}';
out = (template).format([list,head,href,tail,content,end]);
// Check for filter links
} else if (new RegExp(filterLinks).test(href)) {
var filterBy = href.split("/")[1];
out = head + href + tail + ' title="Filter by ' + filterBy + '">' + content + end;
// Check for mailto links
} else if (new RegExp(mailtoLinks).test(href) || mailtoLinks.test($('<div />').html(href).text())) {
out = anchor;
// If not an in page link then it must be a link to a file
} else if (!(new RegExp(inPageLinks).test(href))) {
if (/.*\.md$/.test(href)) preview = true;
out = head + imdone.getFileHref(imdone.currentProjectId(),href,preview) + tail + '>' + content + end;
}
return out;
};
html = html.replace(new RegExp(links), replaceLinks);
// Replace all gollum links
html = html.replace(new RegExp(gollumLinks), function(link, open, name, close) {
var file = name;
if (/\|/.test(name)) {
var pieces = name.split("|");
file = pieces[1];
name = pieces[0];
}
file = file.replace(/(\s)|(\/)/g,"-") + ".md";
var href = imdone.getFileHref(file,true);
return '<a href="{}">{}</a>'.tokenize(href, name);
});
return html;
};
$(document).on('click', 'a.task-link', function(evt) {
var $el = $(evt.target);
imdone.scrollToTask = $el.text();
imdone.scrollToList = $el.attr('data-list') || $el.closest('a.task-link').attr('data-list');
imdone.navigateToCurrentProject();
evt.preventDefault();
evt.stopPropagation();
});
imdone.getFileHref = function(path, line, preview) {
if (_.isObject(preview)) preview = undefined;
if (_.isObject(line)) line = undefined;
if (line && isNaN(line)) preview = true;
project = imdone.currentProjectId();
path = encodeURIComponent(path);
var href = '#file/{}/{}'.tokenize(project, path);
if (line) href+= ("/" + line);
if (preview) href += "/true";
return href;
};
imdone.getSearchHref = function(project,query,offset,limit) {
var href = "#search/{}/{}/{}".tokenize(encodeURIComponent(project),encodeURIComponent(query),offset);
if (limit) href += ("/"+limit);
return href;
};
Handlebars.registerHelper('fileHref', imdone.getFileHref);
Handlebars.registerHelper('highlightCode', function(text, keyword) {
text = Handlebars.Utils.escapeExpression(text);
var regex = new RegExp('^(.*)(' + keyword + ')(.*)$', 'i');
var result = text.replace(regex, '<code>$1</code><code class="highlight">$2</code><code>$3</code>');
return new Handlebars.SafeString(result);
});
//#TODO:70 Take a look at this <https://speakerdeck.com/ammeep/unsuck-your-backbone>, <http://amy.palamounta.in/2013/04/12/unsuck-your-backbone/>
imdone.setProjectData = function(project, data) {
imdone.data[project] = data;
imdone.data.cwd = project;
};
imdone.currentProjectId = function(projectId) {
if (projectId) imdone.data.cwd = projectId;
return imdone.data.cwd;
};
imdone.currentProject = function() {
return imdone.data[imdone.currentProjectId()];
};
imdone.currentListNames = function() {
return _.pluck(imdone.currentProject().lists, "name");
};
imdone.isListHidden = function(list) {
return _.findWhere(imdone.currentProject().lists, {name:list}).hidden;
};
imdone.isMD = function(file) {
if (file) {
if (/\.md$/i.test(file)) return true;
else return false;
}
if (imdone.source && /^(md|markdown)$/i.test(imdone.source.lang)) return true;
return false;
};
// PLANNING:90 add notify and undo for move
imdone.moveTasks = function(opts) {
var tasks = [];
var toListId = (opts.to) ? opts.to : opts.item.closest(".list").attr("id");
var pos = (opts.pos !== undefined) ? opts.pos : opts.item.index()-1;
imdone.selectedTasks.each(function() {
//var $el = ($(this) == ui.item) ? ui.item : $(this);
var $el = $(this);
var taskId = $el.attr("data-id");
var listId = $el.attr("data-list");
var path = $el.attr("data-path");
var list = _.findWhere(imdone.currentProject().lists, {name:listId});
var task = _.filter(list.tasks, function(task) {
return task.id == parseInt(taskId, 10) && task.source.path == path;
})[0];
tasks.push(task);
});
var reqObj = {
tasks:tasks,
newList:toListId,
newPos:pos,
project:imdone.currentProjectId()
};
//Now call the service and call getKanban
client.moveTasks(reqObj);
};
imdone.moveTask = function(item) {
var taskId = item.attr("data-id");
var listId = item.attr("data-list");
var path = item.attr("data-path");
var toListId = item.closest(".list").attr("id");
var list = _.findWhere(imdone.currentProject().lists, {name:listId});
var tasks = _.filter(list.tasks, function(task) {
return task.id == parseInt(taskId, 10) && task.source.path == path;
});
var pos = item.index()-1;
var reqObj = {
tasks:tasks,
newList:toListId,
newPos:pos,
project:imdone.currentProjectId()
};
//Now call the service and call getKanban
client.moveTasks(reqObj);
};
imdone.moveList = function(e,ui) {
var name = ui.item.attr("data-list");
var pos = ui.item.index()-1;
var reqObj = { name: name, pos: pos, project: imdone.currentProjectId() };
//Now call the service and call getKanban
client.moveList(reqObj);
};
imdone.hideList = function(list) {
client.hideList(list, project);
};
imdone.showList = function(list, cb) {
client.showList(list, project);
};
imdone.getKanban = function(params) {
//Clear out all elements and event handlers
//Load the most recent data
var project = params && params.project || imdone.currentProjectId();
if (project) {
client.getKanban(project, function(data) {
imdone.setProjectData(project,data);
imdone.tour.setProject(data);
if ((params && !params.noPaint) || params === undefined) imdone.paintKanban(data);
if (params && params.callback && _.isFunction(params.callback)) params.callback(data);
}, function() {
imdone.app.navigate('/', {trigger:true});
});
}
};
imdone.search = function(params) {
var project = params && params.project || imdone.currentProjectId();
if (project) {
var search = new imdone.Search({
id:project,
query:params.query,
offset:parseInt(params.offset, 10),
limit:(params.limit)?parseInt(params.limit, 10):undefined
});
search.fetch({success: function(model, response) {
// #TODO:90 Put search in a view. [What is a view? - Backbone.js Tutorials](http://backbonetutorials.com/what-is-a-view/)
var template = Handlebars.compile($("#search-results-template").html());
var results = model.toJSON();
var last = results.total+results.offset;
var context = {project:project,results:results,last:last};
if (results.offset > 0) {
var offset = results.offset - results.opts.limit;
context.previous = imdone.getSearchHref(project,results.query,offset);
}
if (results.filesNotSearched > 0) {
context.next = imdone.getSearchHref(project,results.query,last);
}
imdone.searchResults.html(template(context));
imdone.showSearchResults();
if (params && params.callback && _.isFunction(params.callback)) params.callback(data);
}
});
}
};
$(document).on('click', '.pager a[href="#"]', function(e) {
e.preventDefault();
e.stopPropagation();
return false;
});
imdone.showSearchResults = function() {
imdone.hideAllContent();
imdone.searchResults.show();
imdone.searchResultsBtn.show()
.addClass("active")
.attr("title", "Hide search results");
};
imdone.hideSearchResults = function(show) {
imdone.searchResults.hide();
imdone.searchResultsBtn.removeClass("active");
if (show) {
if (imdone.editMode) {
imdone.showEditor();
} else {
imdone.paintKanban(imdone.currentProject());
imdone.showBoard();
}
}
imdone.searchResultsBtn.removeClass("active")
.attr("title", "Show search results");
};
imdone.isSearchResultsVisible = function() {
return imdone.searchResults.is(":visible");
};
imdone.showBoard = function() {
imdone.contentNav.hide();
imdone.boardBar.show();
imdone.board.show();
};
imdone.hideBoard = function() {
imdone.boardBar.hide();
imdone.board.hide();
};
imdone.getProjectStore = function() {
var projects = store.get('projects') || {};
this.projectStore = projects[this.currentProjectId()] || {};
return this.projectStore;
};
imdone.saveProjectStore = function() {
var projects = store.get('projects') || {};
projects[this.currentProjectId()] = this.projectStore || {};
store.set('projects', projects);
};
imdone.filter = function(filter) {
$(".task").show();
if (_.isString(filter)) this.filterField.val(filter);
else filter = this.filterField.val();
if (filter) {
imdone.getProjectStore().filter = filter;
imdone.saveProjectStore();
// ARCHIVE:770 Use a regex for filter and create button to filter by files of selected tasks
// $('.task:not([data-path*="{0}"])'.format([filter])).hide();
$('.task').hide();
$('.task:regex(data-path,{0})'.format([filter])).show();
}
};
imdone.filterBySelectedTasks = function() {
if (imdone.selectedTasks.length > 0) {
var paths = [];
imdone.selectedTasks.each(function() {
var path = $(this).attr("data-path");
if (_.indexOf(paths, path) < 0) paths.push(path);
});
var filter = paths.join("|");
imdone.app.navigate('#filter/{0}'.format([filter]), true);
}
};
imdone.clearFilter = function() {
this.filterField.val("");
delete this.getProjectStore().filter;
this.saveProjectStore();
$(".task").show();
};
imdone.tasksSelected = function() {
imdone.selectedTasks = $(".task.selected");
if (imdone.selectedTasks.length > 0) {
imdone.archiveBtn.show();
imdone.filterBtn.show();
if (imdone.tour.isCompleted("archiveAndFilter")) {
imdone.archiveBtn.ClassyWiggle("start",imdone.wiggleOpts);
imdone.filterBtn.ClassyWiggle("start",imdone.wiggleOpts);
}
imdone.tour.start("archiveAndFilter");
}
else {
imdone.archiveBtn.hide();
imdone.filterBtn.hide();
}
};
imdone.paintKanban = function(data) {
if (!data.busy && !imdone.editMode) {
imdone.board.empty();
imdone.contentNav.hide();
imdone.projectNav.show();
imdone.searchResults.hide();
imdone.listsMenu.empty();
var template = Handlebars.compile($("#list-template").html());
imdone.board.html(template(data));
template = Handlebars.compile($("#lists-template").html());
imdone.listsMenu.html(template(data));
// Apply existing filter
var filter = imdone.getProjectStore().filter || "";
imdone.filter(filter);
// Add archiveBtn listener
imdone.archiveBtn.unbind().click(function() {
var list = "archive";
_.each(imdone.currentListNames(), function(name) {
if ((/archive|deleted/i).test(name)) list = name;
});
if (imdone.selectedTasks && imdone.selectedTasks.length > 0) imdone.moveTasks({pos:0, to:list});
});
// Add filterBtn listener
imdone.filterBtn.unbind().click(imdone.filterBySelectedTasks);
// Select tasks and select all
$(".task-select-all").click(function(evt) {
var list = $(this).attr("data-list");
var tasks = $("#" + list + " .task");
if ($(this).hasClass("selected")) {
$(this).removeClass("selected").find("i").removeClass("icomoon-check").addClass("icomoon-check-empty");
tasks.each(function() {
$(this).removeClass("selected");
});
} else {
$(this).addClass("selected").find("i").removeClass("icomoon-check-empty").addClass("icomoon-check");
tasks.each(function() {
if ($(this).is(':visible')) $(this).addClass("selected");
});
}
imdone.tasksSelected();
});
$('.task').mouseup(function(e) {
if ($(e.target).hasClass("source-link") || $(e.target).attr('target') == '_blank') return;
var $el = $(this);
if (!imdone.sortingTasks) {
if ($el.hasClass("selected")) {
$el.removeClass("selected");
} else if ($el.is(':visible')) {
$el.addClass("selected");
}
imdone.tasksSelected();
}
});
// Make Sortable
$(".list").sortable({
delay: 300,
items: ".task",
connectWith: ".list",
start: function(evt, ui) {
imdone.sortingTasks = true;
if (imdone.selectedTasks && imdone.selectedTasks.length > 0) {
imdone.selectedTasks.each(function() {
if ($(this).attr("id") != ui.item.attr("id")) $(this).hide();
});
}
},
stop: function(evt, ui) {
imdone.sortingTasks = false;
if (imdone.selectedTasks && imdone.selectedTasks.length > 1) {
imdone.moveTasks({item:ui.item});
}
else imdone.moveTask(ui.item);
}
}).disableSelection();
imdone.listsMenu.sortable({
delay: 300,
axis: "y",
items: ".list-item",
handle:".js-drag-handle",
stop: imdone.moveList
}).disableSelection();
//Set width of board based on number of visible lists
var totalLists = _.reject(data.lists,function(list) {
return list.hidden;
}).length;
var width = 362*totalLists;
imdone.board.css('width',width + 'px');
imdone.boardBar.show();
if (!imdone.isSearchResultsVisible()) imdone.board.show();
//$('.list-name-container, .list-hide, .list-show, [title]').tooltip({placement:"bottom"});
if (data.readme) {
// ARCHIVE:160 Fix readme href
var href = imdone.getFileHref(data.readme.path,true);
imdone.openReadmeBtn.attr("title", "Open " + data.readme.path + " file.")
.show()
.unbind()
.click(function() {
imdone.app.navigate(href, true);
});
if (imdone.tour && imdone.tour.isCompleted('newProject')) imdone.openReadmeBtn.ClassyWiggle("start",imdone.wiggleOpts);
} else {
imdone.openReadmeBtn.hide();
}
if (imdone.scrollToTask) {
var task = imdone.scrollToTask, list = imdone.scrollToList;
delete imdone.scrollToTask;
delete imdone.scrollToList;
var scrollToTask = function() {
var $task = $('.task:contains("{}")'.tokenize(task));
if ($task.length > 0) {
$task.addClass('selected');
$('.app-container').scrollTo($task, 500);
}
};
scrollToTask();
}
// Check for selected tasks, there shouldn't be any, but it'll hide the buttone
imdone.tasksSelected();
}
};
imdone.getProjects = function(callback) {
client.getProjects(function(data){
imdone.projects = data;
if (data.length > 0) imdone.paintProjectsMenu();
if (_.isFunction(callback)) callback(data);
});
};
imdone.paintProjectsMenu = function() {
imdone.projectsMenu.empty();
var template = Handlebars.compile($("#projects-template").html());
var context = {
cwd: imdone.currentProjectId(),
projects:_.without(imdone.projects, imdone.currentProjectId()).sort()
};
imdone.projectsMenu.html(template(context)).show();
};
imdone.currentFileChanged = function(data) {
// Check if the current file is being modified
if (data.mods.length > 0) {
var fileUpdate = _.find(data.mods, function(mod) { return mod.mod === 'file.update' });
if (fileUpdate && imdone.source && fileUpdate.file === imdone.source.path) {
client.getFile(imdone.currentProjectId(), imdone.source.path, function(data) {
if (data.src !== imdone.source.src) {
imdone.reloadFileConfirm(function(reload) {
if (reload) imdone.getSource({
path: imdone.source.path,
preview: imdone.previewMode,
line: imdone.editor.getCursorPosition().row+1
});
});
}
// #TODO:20 How do we check for deleted???
});
}
}
};
imdone.initUpdate = function() {
client.initUpdate({
'project.modified': function(data) {
var projectId = data.project;
console.log("Project modified: ", projectId);
var currentProjectId = imdone.currentProjectId();
if (_.indexOf(imdone.projects, projectId) < 0) return;
var boardHidden = !imdone.board.is(':visible');
// only react if project exists and is current.
if (projectId == currentProjectId) {
console.log("boardHidden:", boardHidden);
imdone.currentFileChanged(data);
imdone.getKanban({
project:projectId,
noPaint:boardHidden,
callback:function() {
console.log("refresh of " + projectId + " complete!");
}
});
}
},
'files.processed': function(data) {
var pcntNum = Math.round((data.processed/data.total)*100);
var pcnt = pcntNum.toString() + '%';
var $bar = imdone.progress.find('.bar');
$bar.css('width', pcnt);
},
'project.initialized': function(data) {
// add the project and get kanban
var projectId = data.project;
console.log("Project initialized: ", projectId);
setTimeout(function() {
imdone.progress.modal('hide');
imdone.progress.find('.bar').css('width', '0%');
}, 1000);
if (_.indexOf(imdone.projects, projectId) < 0) {
imdone.projects.push(projectId);
imdone.paintProjectsMenu();
}
imdone.currentProjectId(projectId);
imdone.navigateToCurrentProject();
},
'project.removed': function(data) {
var projectId = data.project;
console.log("Project removed: ", projectId);
// remove the project
imdone.projects = _.without(imdone.projects, projectId);
delete imdone.data[projectId];
// repaint the projects menu
imdone.paintProjectsMenu();
if (imdone.projects.length === 0) {
imdone.app.navigate('/', {trigger:true});
} else {
imdone.currentProjectId(imdone.projects[0]);
imdone.navigateToCurrentProject();
}
}
});
};
imdone.getFileHistory = function() {
var projectHist;
var hist = store.get('history');
if (hist && hist[imdone.currentProjectId()]) {
projectHist = hist[imdone.currentProjectId()];
projectHist.reverse();
}
return projectHist;
};
imdone.addFileToHistory = function() {
var projectHist;
var hist = store.get('history');
if (!hist) hist = {};
if (!hist[imdone.currentProjectId()]) hist[imdone.currentProjectId()] = [];
//remove other occurences of path
hist[imdone.currentProjectId()] = _.without(hist[imdone.currentProjectId()], imdone.source.path);
projectHist = hist[imdone.currentProjectId()];
projectHist.push(imdone.source.path);
//ARCHIVE:900 Don't pop, shift
if (projectHist.length > 10) projectHist.shift();
store.set('history', hist);
projectHist.reverse();
return projectHist;
};
imdone.addProjectToHistory = function(path) {
var hist = store.get('project-history');
if (!hist) hist = [];
hist = _.without(hist, path);
hist.unshift(path);
if (hist.length > 10) hist.pop();
store.set('project-history', hist);
};
imdone.getProjectHistory = function() {
return store.get('project-history');
};
imdone.removeCurrentFileFromHistory = function() {
var projectHist;
var hist = store.get('history');
if (!hist) return;
if (!hist[imdone.currentProjectId()]) return;
//remove other occurences of path
hist[imdone.currentProjectId()] = _.without(hist[imdone.currentProjectId()], imdone.source.path);
store.set('history', hist);
};
imdone.getSource = function(params) {
params.project = params.project || imdone.currentProjectId();
//ARCHIVE:880 We have to convert the source api url URL first
if (params && params.path) params.path = params.path.replace(/^\/*/,'');
imdone.previewMode = params.preview;
client.getFile(params.project, params.path, params.line, function(data){
imdone.source = data;
imdone.currentProjectId(data.project);
//store the path in history
imdone.addFileToHistory();
//Make sure we have the right project displayed
imdone.paintProjectsMenu();
//ARCHIVE:750 Update file-path on edit button
imdone.filename.empty().html(imdone.source.path);
imdone.editMode = true;
if (imdone.isMD()) {
imdone.previewToggle.show();
} else {
imdone.previewToggle.hide();
}
imdone.hideAllContent();
imdone.hideBoard();
if (imdone.isMD() && imdone.previewMode === true && imdone.source.src !== "") {
imdone.showPreview();
} else {
imdone.showEditor();
}
}, function(error) {
console.log(error);
});
};
imdone.showFileView = function() {
imdone.contentNav.show();
imdone.editBar.show();
};
imdone.parseQueryString = function(queryString) {
var params = {};
if(queryString){
_.each(
_.map(decodeURI(queryString).split(/&/g),function(el,i){
var aux = el.split('='), o = {};
if(aux.length >= 1){
var val;
if(aux.length == 2)
val = aux[1];
o[aux[0]] = val;
}
return o;
}),
function(o){
_.extend(params,o);
}
);
}
return params;
};
//print
imdone.print = function() {
var printOptions = {
pageTitle: imdone.source.path,
importCSS: false,
loadCSS:['/css/print-element.css']
};
if(imdone.previewMode && imdone.source.ext == "md") {
imdone.preview.printThis(printOptions);
} else if (imdone.editMode) {
$("<pre><code>" + imdone.editor.getValue() + "</code></pre>").printThis(printOptions);
} else {
imdone.board.printThis(printOptions);
}
};
imdone.printBtn.on("click", imdone.print);
// ARCHIVE:130 Fix markdown language mode for editor
//Show the editor
imdone.showEditor = function(e) {
if (e) {
e.stopPropagation();
e.preventDefault();
}
imdone.previewMode = false;
imdone.editBtn.addClass("active");
imdone.previewBtn.removeClass("active");
var data = imdone.source,
mode = imdone.modes[data.ext] || "text";
var line = parseInt(data.line, 10);
line = isNaN(line) ? 0 : line;
// ARCHIVE:790 User should be able to set global ace confiuration and have it saved to config.js
var session = imdone.aceSession = ace.createEditSession(data.src);
session.setMode("ace/mode/" + mode);
session.setUseWrapMode(true);
session.setWrapLimitRange(120, 160);
session.setOption('tabSize',2);
//Editor change events
session.on('change', function(e) {
if (imdone.source.src != imdone.editor.getValue()) {
if (!imdone.fileModified) {
if (imdone.fileNotify) imdone.fileNotify.pnotify_remove();
imdone.fileModified = true;
imdone.fileModifiedNotify = $.pnotify({
title: "File modified!",
nonblock: true,
hide: false,
sticker: false,
type: 'warning',
icon: 'icomoon-exclamation-sign'
});
}
} else {
imdone.fileModified = false;
imdone.fileModifiedNotify.pnotify_remove();
}
});
imdone.editor.setSession(session);
imdone.hideAllContent();
imdone.showFileView();
imdone.editorEl.show();
imdone.fileContainer.show({
duration: 0,
complete: function() {
imdone.editor.resize(true);
imdone.editor.gotoLine(line);
imdone.editor.focus();
imdone.tour.start('newFile');
}
});
};
imdone.editBtn.on("click", imdone.showEditor);
imdone.hideAllContent = function() {
imdone.previewContainer.hide();
imdone.fileContainer.hide();
imdone.contentNav.hide();
imdone.hideSearchResults();
imdone.board.hide();
};
//Show the markdown preview
imdone.showPreview = function(e) {
if (e) {
e.stopPropagation();
e.preventDefault();
}
if (imdone.isMD()) {
imdone.previewMode = true;
imdone.showFileView();
imdone.previewBtn.addClass("active");
imdone.editBtn.removeClass("active");
imdone.editor.blur();
imdone.hideAllContent();
imdone.contentNav.show();
imdone.editorEl.hide();
imdone.preview.empty();
imdone.preview.html(imdone.md());
imdone.fileContainer.show();
imdone.previewContainer.show();
imdone.fileContainer.focus();
// setup the clipboard for pre elements
preId = 0;
imdone.preview.find('pre').each(function() {
var id = 'pre-id-' + preId;
var copyButton = $(imdone.copyButton);
copyButton.attr('data-clipboard-target', id);
$(this).attr('id', id);
$(this).before(copyButton);
if (!/language-/.test($(this).attr('class'))) $(this).addClass('language-none');
preId++;
});
var clip = new ZeroClipboard($('.copy-btn'));
clip.on( "load", function(client) {
client.on( "complete", function(client, args) {
$.pnotify({
title: "Text copied!",
nonblock: true,
hide: true,
sticker: false,
type: 'success'
});
});
});
// Highlight code
Prism.highlightAll();
// T.O.C
$("#toc").html('').toc({
'content':'#preview',
'headings': 'h1,h2'
});
// ARCHIVE:100 Fix scrollSpy
imdone.fileContainer.scrollspy('refresh');
// Add borders to tables
imdone.preview.find("table").addClass("table table-bordered table-nonfluid");
} else {
imdone.previewMode = false;
}
};
// ARCHIVE:90 Fix toc click
$(document).on('click', '#toc a', function(e) {
var id = $(this).attr('href');
imdone.fileContainer.scrollTo($(id), 500);
e.preventDefault();
e.stopPropagation();
return false;
});
imdone.previewBtn.on("click", function() {
imdone.closeFileConfirm(imdone.showPreview);
});
imdone.fileContainer.scrollspy({ target: '#sidebar'});
//ARCHIVE:950 User should be notified when a file has been modified
imdone.closeFile = function() {
imdone.editMode = false;
imdone.fileModified = false;
imdone.previewMode = false;
$.pnotify_remove_all();
imdone.fileContainer.hide();
imdone.editBar.hide();
imdone.hideAllContent();
delete imdone.source;
};
imdone.closeFileConfirm = function(cb) {
imdone.closeFileOkBtn.unbind('click');
imdone.closeFileCancelBtn.unbind('click');
if (!imdone.fileModified) {
cb();
} else {
imdone.closeFileCancelBtn.click(function(e) {
imdone.closeFileModal.modal("hide");
imdone.fileModified = false;
imdone.fileModifiedNotify.pnotify_remove();
cb();
return false;
});
imdone.closeFileOkBtn.click(function(e) {
imdone.closeFileModal.modal("hide");
imdone.saveFile(cb);
return false;
});
imdone.closeFileModal.modal("show");
}
};
imdone.closeFileModal.on('shown.bs.modal', function() {
imdone.closeFileOkBtn.focus();
});
imdone.reloadFileConfirm = function(cb) {
imdone.reloadFileOkBtn.unbind('click');
imdone.reloadFileCancelBtn.unbind('click');
imdone.reloadFileCancelBtn.click(function(e) {
imdone.reloadFileModal.modal("hide");
cb(false);
return false;
});
imdone.reloadFileOkBtn.click(function(e) {
imdone.reloadFileModal.modal("hide");
cb(true);
return false;
});
imdone.reloadFileModal.modal("show");
};
imdone.reloadFileModal.on('shown.bs.modal', function() {
imdone.reloadFileOkBtn.focus();
});
//Save source from editor
imdone.saveFile = function(evt) {
imdone.source.src = imdone.editor.getValue();
client.saveFile(imdone.currentProjectId(), imdone.source,
function(data) {
if (imdone.fileModified) {
imdone.fileModified = false;
imdone.fileModifiedNotify.pnotify_remove();
}
imdone.fileNotify = $.pnotify({
title: "File saved!",
nonblock: true,
hide: true,
sticker: false,
type: 'success',
icon: 'icomoon-save'
});
if (_.isFunction(evt)) evt();
});
return true;
};
$(document).on('click', '#save-file-btn', imdone.saveFile);
imdone.removeSourceConfirm = function() {
imdone.removeFileName.html(imdone.source.path);
imdone.removeFileModal.modal("show");
};
imdone.removeSource = function() {
client.removeFile(imdone.currentProjectId(), imdone.source.path,
function(data) {
imdone.removeCurrentFileFromHistory();
imdone.closeFile();
imdone.fileNotify = $.pnotify({
title: "File deleted!",
nonblock: true,
hide: true,
sticker: false,
type: 'success'
});
imdone.navigateToCurrentProject();
},
function(data) {
// PLANNING:30 Make this pnotify default for all errors!
imdone.fileNotify = $.pnotify({
title: "Unable to delete file!",
nonblock: true,
hide: true,
sticker: false,
type: 'error'
});
});
};
//ARCHIVE:890 Implement delete file functionality
imdone.removeFileBtn.on('click', function() {
imdone.removeSourceConfirm();
});
imdone.removeFileOkBtn.on('click', function() {
imdone.removeFileModal.modal("hide");
imdone.removeSource();
return false;
});
imdone.removeFileModal.on('shown.bs.modal', function() {
$('#remove-file-cancel-btn').focus();
});
imdone.navigateToCurrentProject = function() {
imdone.app.navigate("#project/" + imdone.currentProjectId(), {trigger:true});
};
imdone.navigateToFile = function(path, line, preview) {
imdone.app.navigate(imdone.getFileHref(path, line, preview), {trigger:true});
};
imdone.openFileDialog = function(e) {
client.getFiles(imdone.currentProjectId(), function(data) {
imdone.currentProject().ls = data;
imdone.currentProject().cwd = data;
data.history = imdone.getFileHistory();
data.history = _.map(data.history, function(path) {
return {path:path, project:imdone.currentProjectId(), line:null, preview:imdone.isMD(path)};
});
$('#ls').html(imdone.lsTemplate(data));
imdone.fileField.val("");
var fileModal = $('#file-modal').modal({show:false});
fileModal.on('show.bs.modal', function() {
setTimeout(function() {
document.activeElement.blur();
imdone.editor.focus();
}, 500);
});
fileModal.modal("show");
});
};
imdone.getDirs = function(_path, cb) {
_path = (_path === undefined) ? "" : _path;
client.getDirs(_path, cb);
};
imdone.paintProjectDialog = function(_path, cb) {
cb = (cb !== undefined) ? cb : function(){};
imdone.getDirs(_path,function(data) {
data.history = imdone.getProjectHistory();
$('#dirs').html(imdone.dirsTemplate(data));
$('#dir-field').val(data.path);
if (!imdone.showHidden) $('.fs-dir[data-hidden=true]').hide();
cb();
});
};
imdone.openProjectDialog = function(e) {
imdone.paintProjectDialog("", function() {
$('#project-modal').modal();
});
};
imdone.removeProject = function(projectId) {
client.removeProject(projectId, function(err, data) {
if (err) {
console.log(err);
console.log(data);
}
});
};
imdone.openHelp = function(e) {
$.get('/help.md', function(data) {
var help = imdone.md(data);
$('#help-modal').modal({
keyboard: true
}).find('.modal-body').html(help);
});
};
imdone.newList = function(e) {
e.preventDefault();
e.stopPropagation();
imdone.newListField.val("");
imdone.newListField.attr('placeholder', "New list name");
imdone.newListModal.modal('show');
};
imdone.initListNameView = function() {
// Start the list tour
$('#lists-dropdown').on('shown.bs.dropdown', function() {
if ($('.list-item').length > 1) {
imdone.tour.start('moveAndHideLists');
}
});
function listNameFilter(saveFunc) {
return function (e) {
var keyCode = (e.keyCode ? e.keyCode : e.which);
if (keyCode === 13) return saveFunc();
if (!/\w|-/i.test(String.fromCharCode(keyCode))) {
e.preventDefault();
e.stopPropagation();
}
};
}
//Put the focus on the name field when changing list names
imdone.nameModal.modal({show:false});
imdone.nameModal.on('show.bs.modal', function() {
setTimeout(function() {
document.activeElement.blur();
imdone.nameFld.focus();
}, 500);
});
//listen for list name click
$(document).on('click','.list-name', function(e) {
var name = $(this).attr("data-list");
imdone.nameModal.modal('show');
imdone.nameFld.val(name);
imdone.nameFld.attr('placeholder', name);
e.preventDefault();
e.stopPropagation();
});
imdone.nameFld.keypress(listNameFilter(saveListName));
function saveListName() {
var name = imdone.nameFld.attr('placeholder'),
newName = imdone.nameFld.val(),
project = imdone.currentProjectId();
if (newName !== "") {
client.renameList(project, name, newName);
}
imdone.nameModal.modal('hide');
}
//Save a list name
$("#list-name-save").click(saveListName);
imdone.newListModal.modal({show:false});
imdone.newListModal.on('show.bs.modal', function() {
setTimeout(function() {
document.activeElement.blur();
imdone.newListField.focus();
}, 500);
});
imdone.newListField.keypress(listNameFilter(saveNewList));
function saveNewList() {
var self = this;
var name = imdone.newListField.val();
if (name !== "") {
client.addList(imdone.currentProjectId(), name, function() {
imdone.newListModal.modal('hide');
imdone.newListField.val("");
});
}
}
imdone.newListSave.click(saveNewList);
$(document).on('click', '.new-list', imdone.newList);
//Remove a list
$(document).on('click','.remove-list', function() {
client.removeList(imdone.currentProjectId(), $(this).attr("data-list"));
});
};
imdone.initEditor = function() {
//Editor config
imdone.editor.setOptions({
enableBasicAutocompletion: true,
enableSnippets: true
});
var langTools = ace.require("ace/ext/language_tools");
var listsCompleter = {
getCompletions: function(editor, session, pos, prefix, callback) {
callback(null, imdone.currentProject().lists.map(function(list, i) {
return {name: list.name, value:list.name + ":0", score: 10000+(i*10), meta: "list"};
}));
}
};
langTools.addCompleter(listsCompleter);
imdone.editor.setTheme("ace/theme/merbivore_soft");
imdone.editor.setHighlightActiveLine(true);
imdone.editor.setPrintMarginColumn(120);
//ARCHIVE:800 Use Vim keyboard bindings
//imdone.editor.setKeyboardHandler(require("ace/keybinding-vim").Vim);
//Ace keyboard handlers
imdone.editor.commands.addCommand({
name: 'saveFile',
bindKey: {win: 'Ctrl-Shift-S', mac: 'Command-Shift-S'},
exec: function(editor) {
imdone.saveFile();
return false;
},
readOnly: false // false if this command should not apply in readOnly mode
});
imdone.editor.commands.addCommand({
name: 'removeSource',
bindKey: {win: 'Ctrl-Shift-X', mac: 'Command-Shift-X'},
exec: function(editor) {
imdone.removeSourceConfirm();
return false;
},
readOnly: false // false if this command should not apply in readOnly mode
});
imdone.editor.commands.addCommand({
name: 'closeFile',
bindKey: {win: 'Esc', mac: 'Esc'},
exec: function(editor) {
imdone.closeFileConfirm(function() {
if (imdone.isMD()) {
imdone.showPreview();
} else {
imdone.navigateToCurrentProject();
}
});
return false;
},
readOnly: false // false if this command should not apply in readOnly mode
});
// ARCHIVE:20 This should ask for a list and order
imdone.editor.commands.addCommand({
name: 'makeTask',
bindKey: {win: 'Ctrl-K', mac: 'Command-K'},
exec: function(editor) {
var row = editor.getCursorPosition().row; //returns { row:n, column:n }
var session = editor.getSession();
var line = session.getLine(row);
var taskLine = line.replace(/(^[\s\W\d\.]*)(\w*.*$)/i, '$1[$2](#)');
editor.find(line, {
start: {row:row, column:0}
});
editor.replace(taskLine);
var col = editor.getCursorPosition().column;
editor.moveCursorTo(row, col-1);
editor.clearSelection();
},
readOnly: false
});
};
imdone.initKeyHandlers = function() {
// keyboard handlers --------------------------------------------------------------------------------------------
// edit
$(window).bind('keydown', 'I', function(e){
if (imdone.previewMode && imdone.editMode) imdone.showEditor();
e.preventDefault();
e.stopPropagation();
return false;
})
.bind('keydown', 'esc', function(e){
if (!imdone.previewMode && !imdone.editMode) imdone.clearFilter();
imdone.navigateToCurrentProject();
e.preventDefault();
e.stopPropagation();
return false;
})
// delete file
.bind('keydown', 'Ctrl+Shift+X', function(e) {
if (imdone.editMode) {
imdone.removeSourceConfirm();
}
e.preventDefault();
e.stopPropagation();
return false;
})
// search
.bind('keydown', 'Ctrl+Shift+F', function(e) {
imdone.searchBtn.dropdown('toggle');
})
// new list
.bind('keydown', 'Ctrl+Shift+L', imdone.newList)
// open file
.bind('keydown', 'Ctrl+I', imdone.openFileDialog)
// Add a project
.bind('keydown', 'Ctrl+Shift+1', imdone.openProjectDialog)
// Open help
.bind('keydown', 'Shift+/', imdone.openHelp);
};
// ARCHIVE:820 Clean up init before implementing backbone views
imdone.init = function() {
imdone.progress = $('.imdone-progress').modal({
backdrop: 'static',
show: false
});
imdone.tour = new Tour();
imdone.initListNameView();
imdone.initEditor();
imdone.initKeyHandlers();
//Get the file source for a task
$(document).on('click','.source-link', function(e) {
var list = $(this).attr("data-list");
var order = $(this).closest('.task').attr("data-order");
var content = $(this).closest(".task").find('.task-text').html();
var template = '<a href="#{0}:{1}" class="task-link" data-list="{0}"><span class="task-content">{2}</span></a>';
//ARCHIVE:380 Show the current task as notification with <http://pinesframework.org/pnotify/>
$.pnotify({
title: list,
text: template.format([list,order,content]),
nonblock: false,
hide: false,
sticker: false,
icon: 'icomoon-tasks',
type: 'info'
});
});
$('#key-help-link').click(function(e) {
e.preventDefault();
imdone.openHelp();
});
//close the source
imdone.closeFileBtn.on('click', function(e) {
imdone.closeFileConfirm(function() {
imdone.navigateToCurrentProject();
});
e.preventDefault();
return false;
});
//Open or create a file
$(document).on('click','#open-file-btn',imdone.openFileDialog);
//Find a path in files API response node
function findDir(path, node) {