define([ 'jquery', '/bower_components/textpatcher/TextPatcher.js', '/common/toolbar3.js', '/common/cryptpad-common.js', '/common/common-util.js', '/common/cryptget.js', '/bower_components/nthen/index.js', '/common/sframe-common.js', '/common/common-realtime.js', '/customize/application_config.js', '/common/sframe-chainpad-listmap.js', '/customize/pages.js', '/poll/render.js', '/common/diffMarked.js', '/common/sframe-common-codemirror.js', 'cm/lib/codemirror', 'cm/addon/display/placeholder', 'cm/mode/markdown/markdown', 'css!cm/lib/codemirror.css', '/bower_components/file-saver/FileSaver.min.js', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', 'less!/bower_components/components-font-awesome/css/font-awesome.min.css', 'less!/customize/src/less2/main.less', ], function ( $, TextPatcher, Toolbar, Cryptpad, Util, Cryptget, nThen, SFCommon, CommonRealtime, AppConfig, Listmap, Pages, Renderer, DiffMd, SframeCM, CMeditor) { var Messages = Cryptpad.Messages; var saveAs = window.saveAs; var APP = window.APP = { unlocked: { row: [], col: [] }, readOnly: false, Cryptpad: Cryptpad, mobile: function () { return $('body').width() <= 600; } // Menu and content area are not inline-block anymore for mobiles }; var Render = Renderer(Cryptpad, APP); var debug = $.noop; //console.log; var HIDE_INTRODUCTION_TEXT = "hide-text"; var metadataMgr; var Title; var common; var copyObject = function (obj) { return JSON.parse(JSON.stringify(obj)); }; var getCSV = APP.getCSV = function () { if (!APP.proxy) { return; } var data = copyObject(APP.proxy.content); var res = ''; var escapeStr = function (str) { return '"' + str.replace(/"/g, '""') + '"'; }; [null].concat(data.rowsOrder).forEach(function (rowId, i) { [null].concat(data.colsOrder).forEach(function (colId, j) { // thead if (i === 0) { if (j === 0) { res += ','; return; } if (!colId) { throw new Error("Invalid data"); } res += escapeStr(data.cols[colId] || Messages.anonymous) + ','; return; } // tbody if (!rowId) { throw new Error("Invalid data"); } if (j === 0) { res += escapeStr(data.rows[rowId] || Messages.poll_optionPlaceholder) + ','; return; } if (!colId) { throw new Error("Invalid data"); } res += (data.cells[colId + '_' + rowId] || 3) + ','; }); // last column: total // thead if (i === 0) { res += escapeStr(Messages.poll_total) + '\n'; return; } // tbody if (!rowId) { throw new Error("Invalid data"); } res += APP.count[rowId] || '?'; res += '\n'; }); return res; }; var exportFile = function () { var csv = getCSV(); var suggestion = Title.suggestTitle(Title.defaultTitle); Cryptpad.prompt(Messages.exportPrompt, Cryptpad.fixFileName(suggestion) + '.csv', function (filename) { if (!(typeof(filename) === 'string' && filename)) { return; } var blob = new Blob([csv], {type: "application/csv;charset=utf-8"}); saveAs(blob, filename); }); }; /* Make sure that the realtime data structure has all the required fields */ var prepareProxy = function (proxy, schema) { if (proxy && proxy.version === 1) { return; } debug("Configuring proxy schema..."); proxy.metadata = proxy.metadata || schema.metadata; Object.keys(schema.metadata).forEach(function (k) { if (!proxy.metadata[k]) { proxy.metadata[k] = schema.metadata[k]; } }); proxy.content = proxy.content || schema.content; Object.keys(schema.content).forEach(function (k) { if (!proxy.content[k]) { proxy.content[k] = schema.content[k]; } }); proxy.version = 1; proxy.type = 'poll'; }; /* Set the user id (user column) in the pad attributes */ var setUserId = function (id, cb) { cb = cb || $.noop; APP.userid = id; common.setPadAttribute('userid', id, function (e) { if (e) { console.error(e); return void cb(e); } cb(); }); }; var sortColumns = function (order, firstcol) { var colsOrder = order.slice(); // Never put at the first position an uncommitted column if (APP.proxy.content.colsOrder.indexOf(firstcol) === -1) { return colsOrder; } colsOrder.sort(function (a, b) { return (a === firstcol) ? -1 : ((b === firstcol) ? 1 : 0); }); return colsOrder; }; var isUncommitted = function (id) { var idArr = id.split('_'); var idx = idArr[0]; var idy = idArr[1] || idArr[0]; // if id is y-{...} (no 'x'), use idArr[0] as 'y' coordinate return APP.uncommitted.content.colsOrder.indexOf(idx) !== -1 || APP.uncommitted.content.rowsOrder.indexOf(idy) !== -1; }; var mergeUncommitted = function (proxy, uncommitted, commit) { var newObj; if (commit) { newObj = proxy; } else { newObj = copyObject(proxy); } // Merge uncommitted into the proxy uncommitted.content.colsOrder = uncommitted.content.colsOrder || []; uncommitted.content.colsOrder.forEach(function (x) { if (newObj.content.colsOrder.indexOf(x) !== -1) { return; } newObj.content.colsOrder.push(x); }); for (var k in uncommitted.content.cols) { if (!newObj.content.cols[k]) { newObj.content.cols[k] = uncommitted.content.cols[k]; } } for (var l in uncommitted.content.cells) { if (!newObj.content.cells[l]) { newObj.content.cells[l] = uncommitted.content.cells[l]; } } // Uncommitted rows uncommitted.content.rowsOrder = uncommitted.content.rowsOrder || []; uncommitted.content.rowsOrder.forEach(function (x) { if (newObj.content.rowsOrder.indexOf(x) !== -1) { return; } newObj.content.rowsOrder.push(x); }); for (var m in uncommitted.content.rows) { if (!newObj.content.rows[m]) { newObj.content.rows[m] = uncommitted.content.rows[m]; } } if (commit) { APP.uncommited = {}; prepareProxy(APP.uncommitted, copyObject(Render.Example)); } return newObj; }; var enableColumn = APP.enableColumn = function (id, table) { table = table || $('body'); var $input = $(table).find('input[disabled="disabled"][data-rt-id^="' + id + '"]') .removeAttr('disabled'); $input.closest('td').addClass('cp-app-poll-table-editing'); $(table).find('.cp-app-poll-table-lock[data-rt-id="' + id + '"]').addClass('fa-unlock') .removeClass('fa-lock').attr('title', Messages.poll_unlocked); }; var disableColumn = function (id) { var $input = $('input[data-rt-id^="' + id + '"]') .attr('disabled', 'disabled'); $input.closest('td').removeClass('cp-app-poll-table-editing'); $('.cp-app-poll-table-lock[data-rt-id="' + id + '"]').addClass('fa-lock') .removeClass('fa-unlock').attr('title', Messages.poll_locked); }; var enableRow = APP.enableRow = function (id, table) { table = table || $('body'); var $input = $(table).find('input[disabled="disabled"][data-rt-id="' + id + '"]') .removeAttr('disabled'); $input.closest('td').addClass('cp-app-poll-table-editing'); $(table).find('span.cp-app-poll-table-edit[data-rt-id="' + id + '"]') .css('visibility', 'hidden'); }; var disableRow = function (id) { var $input = $('input[type="text"][data-rt-id="' + id + '"]') .attr('disabled', 'disabled'); $input.closest('td').removeClass('cp-app-poll-table-editing'); $('span.cp-app-poll-table-edit[data-rt-id="' + id + '"]').css('visibility', 'visible'); }; var unlockElements = function () { APP.unlocked.row.forEach(enableRow); APP.unlocked.col.forEach(enableColumn); }; var setTablePublished = function (bool) { if (bool) { if (APP.$publish) { APP.$publish.hide(); } if (APP.$admin) { APP.$admin.show(); } $('#cp-app-poll-form').addClass('cp-app-poll-published'); } else { if (APP.$publish) { APP.$publish.show(); } if (APP.$admin) { APP.$admin.hide(); } $('#cp-app-poll-form').removeClass('cp-app-poll-published'); } }; var addScrollClass = function () { var $scroll = $('#cp-app-poll-table-scroll'); var hasScroll = $scroll.width() < $scroll[0].scrollWidth && $scroll.width() > 100; if (hasScroll) { $scroll.addClass('cp-app-poll-table-scrolled'); return; } $scroll.removeClass('cp-app-poll-table-scrolled'); }; var updateTableButtons = function () { var uncomColId = APP.uncommitted.content.colsOrder[0]; var uncomRowId = APP.uncommitted.content.rowsOrder[0]; var $createOption = $('tbody input[data-rt-id="' + uncomRowId+'"]') .closest('td').find('> div'); $createOption.find('#cp-app-poll-create-option').remove(); $createOption.append(APP.$createRow); var $createUser = $('thead input[data-rt-id="' + uncomColId + '"]') .closest('td'); $createUser.find('#cp-app-poll-create-user').remove(); $createUser.prepend(APP.$createCol); }; var updateDisplayedTable = function () { setTablePublished(APP.proxy.published); addScrollClass(); updateTableButtons(); }; var unlockColumn = function (id, cb) { if (APP.unlocked.col.indexOf(id) === -1) { APP.unlocked.col.push(id); } enableColumn(id); if (typeof(cb) === "function") { cb(); } }; var unlockRow = function (id, cb) { if (APP.unlocked.row.indexOf(id) === -1) { APP.unlocked.row.push(id); } enableRow(id); if (typeof(cb) === "function") { cb(); } }; var lockColumn = function (id, cb) { var idx = APP.unlocked.col.indexOf(id); if (idx !== -1) { APP.unlocked.col.splice(idx, 1); } disableColumn(id); if (typeof(cb) === "function") { cb(); } }; var lockRow = function (id, cb) { var idx = APP.unlocked.row.indexOf(id); if (idx !== -1) { APP.unlocked.row.splice(idx, 1); } disableRow(id); if (typeof(cb) === "function") { cb(); } }; /* Any time the realtime object changes, call this function */ var change = function (o, n, path, throttle, cb) { if (path && !Cryptpad.isArray(path)) { return; } if (path && path.join) { debug("Change from [%s] to [%s] at [%s]", o, n, path.join(', ')); } var table = APP.$table[0]; common.notify(); var getFocus = function () { var active = document.activeElement; if (!active) { return; } return { el: active, id: $(active).attr('data-rt-id'), start: active.selectionStart, end: active.selectionEnd }; }; var setFocus = function (obj) { var el; if (document.body.contains(obj.el)) { el = obj.el; } else if($('input[data-rt-id="' + obj.id + '"]').length) { el = $('input[data-rt-id="' + obj.id + '"]')[0]; } else { return; } el.focus(); if (obj.start) { el.selectionStart = obj.start; } if (obj.end) { el.selectionEnd = obj.end; } }; var updateTable = function () { var displayedObj = mergeUncommitted(APP.proxy, APP.uncommitted); var colsOrder = sortColumns(displayedObj.content.colsOrder, APP.userid); var conf = { cols: colsOrder, readOnly: APP.readOnly }; var f = getFocus(); APP.$createRow.detach(); APP.$createCol.detach(); Render.updateTable(table, displayedObj, conf); // Fix autocomplete bug: displayedObj.content.rowsOrder.forEach(function (rowId) { if (f.id === rowId) { return; } $('input[data-rt-id="' + rowId +'"]').val(displayedObj.content.rows[rowId] || ''); }); displayedObj.content.colsOrder.forEach(function (colId) { if (f.id === colId) { return; } $('input[data-rt-id="' + colId +'"]') .val(displayedObj.content.cols[colId] || ''); }); updateDisplayedTable(); setFocus(f); if (typeof(cb) === "function") { cb(); } }; if (throttle) { if (APP.throttled) { window.clearTimeout(APP.throttled); } APP.throttled = window.setTimeout(function () { updateTable(); }, throttle); return; } window.setTimeout(updateTable); }; var getRealtimeId = function (input) { return input.getAttribute && input.getAttribute('data-rt-id'); }; var handleBookmark = function (id) { setUserId(id === APP.userid ? '' : id, change); }; /* Called whenever an event is fired on an input element */ var handleInput = function (input) { var type = input.type.toLowerCase(); var id = getRealtimeId(input); debug(input); var object = APP.proxy; var x = Render.getCoordinates(id)[0]; if (isUncommitted(id)) { object = APP.uncommitted; } switch (type) { case 'text': debug("text[rt-id='%s'] [%s]", id, input.value); Render.setValue(object, id, input.value); change(null, null, null, 1000); break; case 'number': debug("checkbox[tr-id='%s'] %s", id, input.value); if (APP.unlocked.col.indexOf(x) >= 0 || x === APP.userid) { var value = parseInt(input.value); if (isNaN(value)) { console.error("Got NaN?!"); break; } Render.setValue(object, id, value); change(); } else { debug('checkbox locked'); } break; default: debug("Input[type='%s']", type); break; } }; var hideInputs = function (id) { if (APP.readOnly) { return; } if (id) { var type = Render.typeofId(id); if (type === 'col') { return void lockColumn(id); } if (type === 'row') { return void lockRow(id); } return; } var keepColUnlocked = [APP.userid, APP.uncommitted.content.colsOrder[0]]; var keepRowUnlocked = APP.uncommitted.content.rowsOrder.slice(); var toLock = []; APP.unlocked.col.forEach(function (id) { if (keepColUnlocked.indexOf(id) !== -1) { return; } toLock.push(id); }); toLock.forEach(lockColumn); toLock = []; APP.unlocked.row.forEach(function (id) { if (keepRowUnlocked.indexOf(id) !== -1) { return; } toLock.push(id); }); toLock.forEach(lockRow); }; /* Called whenever an event is fired on a span */ var handleSpan = function (span) { if (!span) { return; } var id = span.getAttribute('data-rt-id'); var type = Render.typeofId(id); var isRemove = span.className && span.className.split(' ') .indexOf('cp-app-poll-table-remove') !== -1; var isEdit = span.className && span.className.split(' ') .indexOf('cp-app-poll-table-edit') !== -1; var isBookmark = span.className && span.className.split(' ') .indexOf('cp-app-poll-table-bookmark') !== -1; var isLock = span.className && span.className.split(' ') .indexOf('cp-app-poll-table-lock') !== -1; var isLocked = span.className && span.className.split(' ').indexOf('fa-lock') !== -1; if (type === 'row') { if (isRemove) { Cryptpad.confirm(Messages.poll_removeOption, function (res) { if (!res) { return; } Render.removeRow(APP.proxy, id, function () { change(); }); }); } else if (isEdit) { unlockRow(id, function () { $('input[data-rt-id="' + id + '"]').focus(); }); } } else if (type === 'col') { if (isRemove) { Cryptpad.confirm(Messages.poll_removeUser, function (res) { if (!res) { return; } Render.removeColumn(APP.proxy, id, function () { change(); if (id === APP.userid) { setUserId(''); } }); }); } else if (isBookmark) { handleBookmark(id); } else if (isLock && isLocked) { unlockColumn(id, function () { $('input[data-rt-id="' + id + '"]').focus(); }); } else if (isLock) { lockColumn(id); } } else if (type === 'cell') { change(); } else { debug("UNHANDLED"); } }; var optionOrder = { 3: 1, // ? => ✔ 1: 2, // ✔ => ~ 2: 0, // ~ => x 0: 3, // x => ? // undefined => 3 }; var handleClick = function (e, isKeyup) { if (APP.readOnly) { return; } e.stopPropagation(); if (!APP.ready) { return; } if (!isKeyup && e.which !== 1) { return; } // only allow left clicks var target = e && e.target; if (!target) { return void debug("NO TARGET"); } var nodeName = target && target.nodeName; switch (nodeName) { case 'INPUT': if ($(target).is('[type="text"]') && !isKeyup) { return; } if (isKeyup && (e.keyCode === 13 || e.keyCode === 27)) { var id = target.getAttribute('data-rt-id'); if ($(target).parents('.cp-app-poll-table-uncommitted').length && e.keyCode === 13) { var type = Render.typeofId(id); if (type === "row") { APP.$createRow.click(); } else if (type === "col") { APP.$createCol.click(); } break; } hideInputs(id); break; } if ($(target).is('input[type="number"]')) { // Nothing to do... //console.error("number input focused?"); break; } handleInput(target); break; case 'LABEL': var input = $('input[type="number"][id=' + $(target).attr('for') + ']'); var value = parseInt(input.val()); input.val(optionOrder[value]); handleInput(input[0]); break; case 'SPAN': handleSpan(target); break; case undefined: //console.error(new Error("C'est pas possible!")); break; default: debug(target, nodeName); break; } }; /* */ var updatePublishButton = function () { if (!APP.ready || !APP.proxy || !APP.$publishButton) { return; } var p = APP.proxy.published; var msg = (p ? Messages.poll_admin_button : Messages.poll_publish_button); APP.$publishButton.attr('title', msg); if (p) { APP.$publishButton.removeClass('fa-check').addClass('fa-pencil'); return; } APP.$publishButton.addClass('fa-check').removeClass('fa-pencil'); }; var publish = APP.publish = function (bool) { if (!APP.readOnly) { if (!APP.ready) { return; } if (APP.proxy.published !== bool) { APP.proxy.published = bool; } } else { // If readOnly, always put the app in published mode bool = true; } $(APP.$mediaTagButton).toggle(!bool); setTablePublished(bool); /*['textarea'].forEach(function (sel) { $(sel).attr('disabled', bool); });*/ updatePublishButton(); APP.editor.refresh(); }; var updateHelpButton = function () { if (!APP.$helpButton) { return; } var help = $('#cp-app-poll-help').is(':visible'); var msg = (help ? Messages.poll_hide_help_button : Messages.poll_show_help_button); APP.$helpButton.attr('title', msg); if (help) { APP.$helpButton.addClass('cp-toolbar-button-active'); return; } APP.$helpButton.removeClass('cp-toolbar-button-active'); }; var showHelp = function(help) { if (typeof help === 'undefined') { help = !$('#cp-app-poll-help').is(':visible'); } var msg = (help ? Messages.poll_hide_help_button : Messages.poll_show_help_button); $('#cp-app-poll-help').toggle(help); $('#cp-app-poll-action-help').text(msg); updateHelpButton(); }; var setEditable = function (editable) { APP.readOnly = !editable; if (editable === false) { // disable all the things $('.icp-app-poll-realtime input, .cp-app-poll-realtime button, .cp-app-poll-upper button, .cp-app-poll-realtime textarea').attr('disabled', true); $('span.cp-app-poll-table-edit, span.cp-app-poll-table-remove').hide(); $('span.cp-app-poll-table-lock').addClass('fa-lock').removeClass('fa-unlock') .attr('title', Messages.poll_locked) .css({'cursor': 'default'}); } else { // enable $('span.cp-app-poll-table-edit, span.cp-app-poll-table-remove').show(); $('span.cp-app-poll-table-lock').css({'cursor': ''}); $('.cp-app-poll-realtime button, .cp-app-poll-upper button, .cp-app-poll-realtime textarea').attr('disabled', false); unlockElements(); } }; var updatePublishedDescription = function () { var v = APP.editor.getValue(); DiffMd.apply(DiffMd.render(v || ''), APP.$descriptionPublished); }; var updateDescription = function (old, n) { var o = APP.editor.getValue(); SframeCM.setValueAndCursor(APP.editor, o, n, TextPatcher); updatePublishedDescription(); common.notify(); }; var updateLocalDescription = function (n) { APP.proxy.description = n; updatePublishedDescription(); }; var getCommentId = Render.Uid('c'); var removeComment = function (uid) { delete APP.proxy.comments[uid]; APP.updateComments(); }; /*var editComment = function (id) { // TODO };*/ var avatars = {}; var updateComments = APP.updateComments = function () { if (!APP.proxy.comments) { APP.proxy.comments = {}; } var profile; if (common.isLoggedIn()) { profile = metadataMgr.getUserData().profile; } var $comments = APP.$comments.html(''); var comments = APP.proxy.comments; Object.keys(copyObject(comments)).sort(function (a, b) { return comments[a].time > comments[b].time; }).forEach(function (k) { var c = comments[k]; var name = c.name || Messages.anonymous; var $c = $('