diff --git a/customize.dist/main.css b/customize.dist/main.css index 2455aa465..1d3e3c347 100644 --- a/customize.dist/main.css +++ b/customize.dist/main.css @@ -318,47 +318,68 @@ tbody td:last-child { color: #FF0073; cursor: pointer !important; } -form.realtime { +form.realtime, +div.realtime { padding: 0px; margin: 0px; } -form.realtime > textarea { +form.realtime > textarea, +div.realtime > textarea { width: 50%; height: 15vh; } -form.realtime table { +form.realtime table, +div.realtime table { border-collapse: collapse; + width: calc(100% - 1px); } -form.realtime table tr td { +form.realtime table tr td:first-child, +div.realtime table tr td:first-child { + position: absolute; + left: 29px; + top: auto; + width: calc(30% - 50px); +} +form.realtime table tr td, +div.realtime table tr td { padding: 0px; margin: 0px; } -form.realtime table tr td div.text-cell { +form.realtime table tr td div.text-cell, +div.realtime table tr td div.text-cell { padding: 0px; margin: 0px; height: 100%; } -form.realtime table tr td div.text-cell input { +form.realtime table tr td div.text-cell input, +div.realtime table tr td div.text-cell input { width: 80%; + width: 90%; height: 100%; border: 0px; } -form.realtime table tr td div.text-cell input[disabled] { +form.realtime table tr td div.text-cell input[disabled], +div.realtime table tr td div.text-cell input[disabled] { background-color: transparent; color: #fafafa; font-weight: bold; } -form.realtime table tr td.checkbox-cell { +form.realtime table tr td.checkbox-cell, +div.realtime table tr td.checkbox-cell { margin: 0px; padding: 0px; + height: 100%; + min-width: 150px; } -form.realtime table tr td.checkbox-cell div.checkbox-contain { - display: block; +form.realtime table tr td.checkbox-cell div.checkbox-contain, +div.realtime table tr td.checkbox-cell div.checkbox-contain { + display: inline-block; height: 100%; width: 100%; position: relative; } -form.realtime table tr td.checkbox-cell div.checkbox-contain label { +form.realtime table tr td.checkbox-cell div.checkbox-contain label, +div.realtime table tr td.checkbox-cell div.checkbox-contain label { background-color: transparent; display: block; position: absolute; @@ -367,72 +388,138 @@ form.realtime table tr td.checkbox-cell div.checkbox-contain label { height: 100%; width: 100%; } -form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) { +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable), +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) { display: none; } -form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover { +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover { font-weight: bold; background-color: #FF0073; color: #302B28; display: block; } -form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover:after { +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover:after, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover:after { + height: 100%; +} +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover:after, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover:after { content: "✖"; } -form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.yes { +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.yes, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.yes { background-color: #46E981; } -form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.yes:after { +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.yes:after, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.yes:after { content: "✔"; } -form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.mine { +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.uncommitted, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.uncommitted { + background: #ddd; +} +form.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.mine, +div.realtime table tr td.checkbox-cell div.checkbox-contain input[type="checkbox"]:not(.editable) ~ .cover.mine { display: none; } -form.realtime table input[type="text"] { +form.realtime table input[type="text"], +div.realtime table input[type="text"] { height: 100%; + border: 1px solid #302B28; width: 80%; - border: 3px solid #302B28; } -form.realtime table .edit { +form.realtime table thead td, +div.realtime table thead td { + padding: 0px 5px; + background: #4c443f; + border-radius: 20px 20px 0 0; + text-align: center; +} +form.realtime table thead td input[type="text"], +div.realtime table thead td input[type="text"] { + width: 100%; + box-sizing: border-box; +} +form.realtime table thead td input[type="text"][disabled], +div.realtime table thead td input[type="text"][disabled] { + color: white; + padding: 1px 5px; + border: none; +} +form.realtime table tbody .text-cell, +div.realtime table tbody .text-cell { + background: #4c443f; +} +form.realtime table tbody .text-cell input[type="text"], +div.realtime table tbody .text-cell input[type="text"] { + width: calc(100% - 50px); +} +form.realtime table tbody .text-cell .edit, +div.realtime table tbody .text-cell .edit { + float: right; + margin: 0 10px 0 0; +} +form.realtime table tbody .text-cell .remove, +div.realtime table tbody .text-cell .remove { + float: left; + margin: 0 0 0 10px; +} +form.realtime table .edit, +div.realtime table .edit { color: #46E981; cursor: pointer; - width: 10%; - font-size: 20px; -} -form.realtime table .edit:after { - content: '✐'; + float: left; + margin-left: 10px; + /*&:after { content: '✐'; }*/ + /*&.editable { display: none; }*/ } -form.realtime table .edit.editable { - display: none; +form.realtime table .remove, +div.realtime table .remove { + float: right; + margin-right: 10px; } -form.realtime table thead tr th input[type="text"][disabled] { +form.realtime table thead tr th input[type="text"][disabled], +div.realtime table thead tr th input[type="text"][disabled] { background-color: transparent; color: #fafafa; font-weight: bold; } -form.realtime table thead tr th .remove { +form.realtime table thead tr th .remove, +div.realtime table thead tr th .remove { cursor: pointer; font-size: 20px; } -form.realtime table tfoot tr td { +form.realtime table tfoot tr, +div.realtime table tfoot tr { + border: none; +} +form.realtime table tfoot tr td, +div.realtime table tfoot tr td { + border: none; text-align: center; } -form.realtime table tfoot tr td .save { +form.realtime table tfoot tr td .save, +div.realtime table tfoot tr td .save { padding: 15px; border-top-left-radius: 5px; border-top-right-radius: 5px; } form.realtime #adduser, -form.realtime #addoption { +div.realtime #adduser, +form.realtime #addoption, +div.realtime #addoption { color: #46E981; border: 1px solid #46E981; padding: 15px; cursor: pointer; } -form.realtime #adduser { +form.realtime #adduser, +div.realtime #adduser { border-top-left-radius: 5px; } -form.realtime #addoption { +form.realtime #addoption, +div.realtime #addoption { border-bottom-left-radius: 5px; } div.modal, diff --git a/customize.dist/src/cryptpad.less b/customize.dist/src/cryptpad.less index 7492114e9..494e7c369 100644 --- a/customize.dist/src/cryptpad.less +++ b/customize.dist/src/cryptpad.less @@ -359,7 +359,7 @@ tbody { cursor: pointer !important; } -form.realtime { +form.realtime, div.realtime { > input { &[type="text"] { @@ -375,7 +375,14 @@ form.realtime { table { border-collapse: collapse; + width: ~"calc(100% - 1px)"; tr { + td:first-child { + position:absolute; + left: 29px; + top: auto; + width: ~"calc(30% - 50px)"; + } td { padding: 0px; margin: 0px; @@ -383,9 +390,11 @@ form.realtime { div.text-cell { padding: 0px; margin: 0px; - height: 100%; + height: 100%; + input { width: 80%; + width: 90%; height: 100%; border: 0px; &[disabled] { @@ -399,9 +408,11 @@ form.realtime { &.checkbox-cell { margin: 0px; padding: 0px; + height: 100%; + min-width: 150px; div.checkbox-contain { - display: block; + display: inline-block; height: 100%; width: 100%; position: relative; @@ -427,6 +438,11 @@ form.realtime { background-color: @cp-red; color: @base; + + &:after { + height: 100%; + } + &:after { content: "✖"; } display: block; @@ -434,6 +450,12 @@ form.realtime { background-color: @cp-green; &:after { content: "✔"; } } + + &.uncommitted { + background: #ddd; + } + + &.mine { display: none; } @@ -449,17 +471,59 @@ form.realtime { input { &[type="text"] { height: 100%; + border: 1px solid @base; width: 80%; - border: 3px solid @base; + } + } + thead { + td { + padding: 0px 5px; + background: @less-light-base; + border-radius: 20px 20px 0 0; + text-align: center; + input { + &[type="text"] { + width: 100%; + box-sizing: border-box; + &[disabled] { + color: white; + padding: 1px 5px; + border: none; + } + } + } + } + } + + tbody { + .text-cell { + background: @less-light-base; + //border-radius: 20px 0 0 20px; + input[type="text"] { + width: ~"calc(100% - 50px)"; + } + .edit { + float:right; + margin: 0 10px 0 0; + } + .remove { + float: left; + margin: 0 0 0 10px; + } } } .edit { color: @cp-green; cursor: pointer; - width: 10%; - font-size: 20px; - &:after { content: '✐'; } - &.editable { display: none; } + float: left; + margin-left: 10px; + /*&:after { content: '✐'; }*/ + /*&.editable { display: none; }*/ + } + + .remove { + float: right; + margin-right: 10px } thead { @@ -486,7 +550,9 @@ form.realtime { } tfoot { tr { + border: none; td { + border: none; text-align: center; .save { padding: 15px; diff --git a/customize.dist/src/toolbar.less b/customize.dist/src/toolbar.less index d8f7b6a6c..c420bdae1 100644 --- a/customize.dist/src/toolbar.less +++ b/customize.dist/src/toolbar.less @@ -271,7 +271,7 @@ margin: 2px 4px 2px 0px; } .cryptpad-userbuttons-container { - display: none; + /*display: none;*/ } } .cryptpad-toolbar-rightside { diff --git a/customize.dist/src/variables.less b/customize.dist/src/variables.less index 14e6de373..c6d555219 100644 --- a/customize.dist/src/variables.less +++ b/customize.dist/src/variables.less @@ -1,5 +1,6 @@ @base: #302B28; @light-base: lighten(@base, 20%); +@less-light-base: lighten(@base, 10%); @fore: #fafafa; @cp-green: #46E981; diff --git a/customize.dist/toolbar.css b/customize.dist/toolbar.css index cfde957a3..02eeff7e7 100644 --- a/customize.dist/toolbar.css +++ b/customize.dist/toolbar.css @@ -279,7 +279,7 @@ margin: 2px 4px 2px 0px; } .cryptpad-toolbar-leftside .cryptpad-userbuttons-container { - display: none; + /*display: none;*/ } .cryptpad-toolbar-rightside { text-align: right; diff --git a/customize.dist/translations/messages.es.js b/customize.dist/translations/messages.es.js index a7b9740b9..0b6de909f 100644 --- a/customize.dist/translations/messages.es.js +++ b/customize.dist/translations/messages.es.js @@ -27,8 +27,8 @@ define(function () { out.readOnly = 'Solo lectura'; out.anonymous = 'Anónimo'; out.yourself = "tú mismo"; - out.anonymousUsers = "usuarios anónimos"; - out.anonymousUser = "usuario anónimo"; + out.anonymousUsers = "editores anónimos"; + out.anonymousUser = "editor anónimo"; out.shareView = "URL de sólo lectura"; out.shareEdit = "Editar URL"; out.users = "Usuarios"; diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 49fd33731..ce5f49928 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -27,8 +27,8 @@ define(function () { out.readonly = 'Lecture seule'; out.anonymous = "Anonyme"; out.yourself = "Vous-même"; - out.anonymousUsers = "utilisateurs anonymes"; - out.anonymousUser = "utilisateur anonyme"; + out.anonymousUsers = "éditeurs anonymes"; + out.anonymousUser = "éditeur anonyme"; out.shareView = "URL de lecture seule"; out.shareEdit = "URL d'édition"; out.users = "Utilisateurs"; @@ -129,6 +129,12 @@ define(function () { out.wizardTitle = "Utiliser l'assistant pour créer votre sondage"; out.wizardConfirm = "Êtes-vous vraiment prêt à ajouter ces options au sondage ?"; + out.poll_publish_button = "Publier"; + out.poll_admin_button = "Administrer"; + out.poll_create_user = "Ajouter un utilisateur"; + out.poll_create_option = "Ajouter une option"; + out.poll_commit = "Valider"; + out.poll_closeWizardButton = "Fermer l'assistant"; out.poll_closeWizardButtonTitle = "Fermer l'assistant"; out.poll_wizardComputeButton = "Générer les options"; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 846e32a16..d97a11fcf 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -29,8 +29,8 @@ define(function () { out.readonly = 'Read only'; out.anonymous = "Anonymous"; out.yourself = "Yourself"; - out.anonymousUsers = "anonymous users"; - out.anonymousUser = "anonymous user"; + out.anonymousUsers = "anonymous editors"; + out.anonymousUser = "anonymous editor"; out.shareView = "Read-only URL"; out.shareEdit = "Edit URL"; out.users = "Users"; @@ -131,6 +131,12 @@ define(function () { out.wizardTitle = "Use the wizard to create your poll"; out.wizardConfirm = "Are you really ready to add these options to your poll?"; + out.poll_publish_button = "Publish"; + out.poll_admin_button = "Admin"; + out.poll_create_user = "Add a new user"; + out.poll_create_option = "Add a new option"; + out.poll_commit = "Commit"; + out.poll_closeWizardButton = "Close wizard"; out.poll_closeWizardButtonTitle = "Close wizard"; out.poll_wizardComputeButton = "Compute Options"; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 64b0a167b..3569902d0 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -798,7 +798,6 @@ define([ case 'editshare': button = $(' + + + +
+
+
+
+
+ + + +
+
diff --git a/www/poll/test/main.js b/www/poll/test/main.js new file mode 100644 index 000000000..8dbc5c683 --- /dev/null +++ b/www/poll/test/main.js @@ -0,0 +1,715 @@ +define([ + '/api/config?cb=' + Math.random().toString(16).substring(2), + '/customize/messages.js?app=poll', + '/bower_components/textpatcher/TextPatcher.js', + '/bower_components/chainpad-listmap/chainpad-listmap.js', + '/bower_components/chainpad-crypto/crypto.js', + '/common/cryptpad-common.js', + '/bower_components/hyperjson/hyperjson.js', + '/poll/test/render.js', + '/common/toolbar.js', + '/common/visible.js', + '/common/notify.js', + '/bower_components/file-saver/FileSaver.min.js', + '/bower_components/jquery/dist/jquery.min.js', + //'/customize/pad.js' +], function (Config, Messages, TextPatcher, Listmap, Crypto, Cryptpad, Hyperjson, Render, Toolbar) { + var $ = window.jQuery; + + var HIDE_INTRODUCTION_TEXT = "hide_poll_text"; + var defaultName; + + var secret = Cryptpad.getSecrets(); + var readOnly = secret.keys && !secret.keys.editKeyStr; + + var APP = window.APP = { + Toolbar: Toolbar, + Hyperjson: Hyperjson, + Render: Render, + $bar: $('#toolbar').css({ border: '1px solid white', background: 'grey', 'margin-bottom': '1vh', }), + editable: { + row: [], + col: [] + } + }; + + var sortColumns = function (order, firstcol) { + var colsOrder = order.slice(); + colsOrder.sort(function (a, b) { + return (a === firstcol) ? -1 : + ((b === firstcol) ? 1 : 0); + }); + return colsOrder; + }; + + var isOwnColumnCommitted = function () { + return APP.proxy && APP.proxy.table.colsOrder.indexOf(APP.userid) !== -1; + }; + + var mergeUncommitted = function (proxy, uncommitted, commit) { + var newObj; + if (commit) { + newObj = proxy; + } else { + newObj = $.extend(true, {}, proxy); + } + // We have uncommitted data only if the user's column is not in the proxy + // If it is already is the proxy, nothing to merge + if (isOwnColumnCommitted()) { + return newObj; + } + // Merge uncommitted into the proxy + uncommitted.table.colsOrder.forEach(function (x) { + if (newObj.table.colsOrder.indexOf(x) !== -1) { return; } + newObj.table.colsOrder.push(x); + }); + for (var k in uncommitted.table.cols) { + if (!newObj.table.cols[k]) { + newObj.table.cols[k] = uncommitted.table.cols[k]; + } + } + for (var l in uncommitted.table.cells) { + if (!newObj.table.cells[l]) { + newObj.table.cells[l] = uncommitted.table.cells[l]; + } + } + return newObj; + }; + + var setColumnDisabled = function (id, state) { + if (!state) { + $('input[data-rt-id^="' + id + '"]').removeAttr('disabled'); + return; + } + $('input[data-rt-id^="' + id + '"]').attr('disabled', 'disabled'); + }; + + var styleUncommittedColumn = function () { + var id = APP.userid; + + // Enable the checkboxes for the user's column (committed or not) + $('input[disabled="disabled"][data-rt-id^="' + id + '"]').removeAttr('disabled'); + $('input[type="checkbox"][data-rt-id^="' + id + '"]').addClass('enabled'); + $('[data-rt-id="' + id + '"] ~ .edit').css('visibility', 'hidden'); + $('.lock[data-rt-id="' + id + '"]').html('🔓'); + + if (isOwnColumnCommitted()) { return; } + $('[data-rt-id^="' + id + '"]').closest('td').addClass("uncommitted"); + $('td.uncommitted .remove, td.uncommitted .edit').css('visibility', 'hidden'); + $('td.uncommitted .cover').addClass("uncommitted"); + $('.uncommitted input[type="text"]').attr("placeholder", Messages.poll_userPlaceholder); + }; + + var unlockElements = function () { + APP.editable.row.forEach(function (id) { + $('input[type="text"][disabled="disabled"][data-rt-id="' + id + '"]').removeAttr('disabled'); + $('span.edit[data-rt-id="' + id + '"]').css('visibility', 'hidden'); + }); + APP.editable.col.forEach(function (id) { + $('input[disabled="disabled"][data-rt-id^="' + id + '"]').removeAttr('disabled'); + $('input[type="checkbox"][data-rt-id^="' + id + '"]').addClass('enabled'); + $('span.edit[data-rt-id="' + id + '"]').css('visibility', 'hidden'); + $('.lock[data-rt-id="' + id + '"]').html('🔓'); + }); + }; + + var updateTableButtons = function () { + if (!isOwnColumnCommitted()) { + $('#commit').show(); + } + + var $createOption = APP.$table.find('tfoot tr td:first-child'); + var $commitCell = APP.$table.find('tfoot tr td:nth-child(2)'); + $createOption.append(APP.$createRow); + $commitCell.append(APP.$commit); + $('#create-user, #create-option').css('display', 'inline-block'); + if (!APP.proxy || !APP.proxy.table.rowsOrder || APP.proxy.table.rowsOrder.length === 0) { $('#create-user').hide(); } + var width = $('#table').outerWidth(); + if (width) { + //$('#create-user').css('left', width + 30 + 'px'); + } + }; + + var setTablePublished = function (bool) { + if (bool) { + if (APP.$publish) { APP.$publish.hide(); } + if (APP.$admin) { APP.$admin.show(); } + $('#create-option').hide(); + $('.remove[data-rt-id^="y"], .edit[data-rt-id^="y"]').hide(); + } else { + if (APP.$publish) { APP.$publish.show(); } + if (APP.$admin) { APP.$admin.hide(); } + $('#create-option').show(); + $('.remove[data-rt-id^="y"], .edit[data-rt-id^="y"]').show(); + } + }; + + var updateDisplayedTable = function () { + styleUncommittedColumn(); + unlockElements(); + updateTableButtons(); + setTablePublished(APP.proxy.published); + }; + + var unlockColumn = function (id, cb) { + if (APP.editable.col.indexOf(id) === -1) { + APP.editable.col.push(id); + } + if (typeof(cb) === "function") { + cb(); + } + }; + var unlockRow = function (id, cb) { + if (APP.editable.row.indexOf(id) === -1) { + APP.editable.row.push(id); + } + if (typeof(cb) === "function") { + cb(); + } + }; + + /* Any time the realtime object changes, call this function */ + var change = function (o, n, path) { + if (path && path.join) { + console.log("Change from [%s] to [%s] at [%s]", + o, n, path.join(', ')); + } + + var table = APP.$table[0]; + + var displayedObj = mergeUncommitted(APP.proxy, APP.uncommitted); + + var colsOrder = sortColumns(displayedObj.table.colsOrder, APP.userid); + var conf = { + cols: colsOrder, + readOnly: readOnly + }; + + //Render.updateTable(table, displayedObj, conf); + + /* FIXME browser autocomplete fills in new fields sometimes + calling updateTable twice removes the autofilled in values + setting autocomplete="off" is not reliable + + https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion + */ + window.setTimeout(function () { + var displayedObj2 = mergeUncommitted(APP.proxy, APP.uncommitted); + Render.updateTable(table, displayedObj2, conf); + updateDisplayedTable(); + }); + }; + + var getRealtimeId = function (input) { + return input.getAttribute && input.getAttribute('data-rt-id'); + }; + + /* Called whenever an event is fired on an input element */ + var handleInput = function (input) { + var type = input.type.toLowerCase(); + var id = getRealtimeId(input); + + console.log(input); + + var object = APP.proxy; + + var x = Render.getCoordinates(id)[0]; + if (type !== "row" && x === APP.userid && APP.proxy.table.colsOrder.indexOf(x) === -1) { + object = APP.uncommitted; + } + + switch (type) { + case 'text': + console.log("text[rt-id='%s'] [%s]", id, input.value); + if (!input.value) { return void console.log("Hit enter?"); } + Render.setValue(object, id, input.value); + change(); + break; + case 'checkbox': + console.log("checkbox[tr-id='%s'] %s", id, input.checked); + if (APP.editable.col.indexOf(x) >= 0 || x === APP.userid) { + Render.setValue(object, id, input.checked); + change(); + } else { + console.log('checkbox locked'); + } + break; + default: + console.log("Input[type='%s']", type); + break; + } + }; + + /* Called whenever an event is fired on a span */ + var handleSpan = function (span) { + var id = span.getAttribute('data-rt-id'); + var type = Render.typeofId(id); + var isRemove = span.className && span.className.split(' ').indexOf('remove') !== -1; + var isEdit = span.className && span.className.split(' ').indexOf('edit') !== -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 () { + change(); + }); + } + } else if (type === 'col') { + if (isRemove) { + Cryptpad.confirm(Messages.poll_removeUser, function (res) { + if (!res) { return; } + Render.removeColumn(APP.proxy, id, function () { + change(); + }); + }); + } else if (isEdit) { + unlockColumn(id, function () { + change(); + }); + } + } else if (type === 'cell') { + change(); + } else { + console.log("UNHANDLED"); + } + }; + + var hideInputs = function (e) { + if ($(e.target).is('[type="text"]')) { + return; + } + $('.lock[data-rt-id!="' + APP.userid + '"]').html('🔒 '); + var $cells = APP.$table.find('thead td:not(.uncommitted), tbody td'); + $cells.find('[type="text"][data-rt-id!="' + APP.userid + '"]').attr('disabled', true); + $('.edit[data-rt-id!="' + APP.userid + '"]').css('visibility', 'visible'); + APP.editable.col = [APP.userid]; + APP.editable.row = []; + }; + + $(window).click(hideInputs); + + var handleClick = function (e, isKeyup) { + e.stopPropagation(); + + if (!APP.ready) { return; } + var target = e && e.target; + + if (isKeyup) { + console.log("Keyup!"); + } + + if (!target) { return void console.log("NO TARGET"); } + + var nodeName = target && target.nodeName; + + if (!$(target).parents('#table tbody').length || $(target).hasClass('edit')) { + hideInputs(e); + } + + switch (nodeName) { + case 'INPUT': + handleInput(target); + break; + case 'SPAN': + //case 'LABEL': + handleSpan(target); + break; + case undefined: + //console.error(new Error("C'est pas possible!")); + break; + default: + console.log(target, nodeName); + break; + } + }; + + /* + Make sure that the realtime data structure has all the required fields + */ + var prepareProxy = function (proxy, schema) { + if (proxy && proxy.version === 1) { return; } + console.log("Configuring proxy schema..."); + + proxy.info = schema.info; + proxy.table = schema.table; + proxy.version = 1; + }; + + + /* + + */ + var publish = APP.publish = function (bool) { + if (!APP.ready) { return; } + if (APP.proxy.published !== bool) { + APP.proxy.published = bool; + } + setTablePublished(bool); + ['textarea'].forEach(function (sel) { + $(sel).attr('disabled', bool); + }); + }; + + var userData = APP.userData = {}; // List of pretty names for all users (mapped with their ID) + var userList; // List of users still connected to the channel (server IDs) + var addToUserData = function(data) { + var users = userList ? userList.users : undefined; + //var userData = APP.proxy.info.userData; + for (var attrname in data) { userData[attrname] = data[attrname]; } + + if (users && users.length) { + for (var userKey in userData) { + if (users.indexOf(userKey) === -1) { delete userData[userKey]; } + } + } + + if(userList && typeof userList.onChange === "function") { + userList.onChange(userData); + } + + APP.proxy.info.userData = userData; + }; + + //var myData = {}; + var getLastName = function (cb) { + Cryptpad.getAttribute('username', function (err, userName) { + cb(err, userName || ''); + }); + }; + + var setName = APP.setName = function (newName) { + if (typeof(newName) !== 'string') { return; } + var myUserNameTemp = Cryptpad.fixHTML(newName.trim()); + if(myUserNameTemp.length > 32) { + myUserNameTemp = myUserNameTemp.substr(0, 32); + } + var myUserName = myUserNameTemp; + var myID = APP.myID; + var myData = {}; + myData[myID] = { + name: myUserName + }; + addToUserData(myData); + Cryptpad.setAttribute('username', newName, function (err, data) { + if (err) { + console.error("Couldn't set username"); + return; + } + APP.userName.lastName = myUserName; + //change(); + }); + }; + + var updateTitle = function (newTitle) { + if (newTitle === document.title) { return; } + // Change the title now, and set it back to the old value if there is an error + var oldTitle = document.title; + document.title = newTitle; + Cryptpad.renamePad(newTitle, function (err, data) { + if (err) { + console.log("Couldn't set pad title"); + console.error(err); + document.title = oldTitle; + return; + } + document.title = data; + APP.$bar.find('.' + Toolbar.constants.title).find('span.title').text(data); + APP.$bar.find('.' + Toolbar.constants.title).find('input').val(data); + }); + }; + + var updateDefaultTitle = function (defaultTitle) { + defaultName = defaultTitle; + APP.$bar.find('.' + Toolbar.constants.title).find('input').attr("placeholder", defaultName); + }; + var renameCb = function (err, title) { + if (err) { return; } + document.title = title; + APP.proxy.info.title = title; + }; + + var suggestName = function (fallback) { + return document.title || defaultName || ""; + }; + + + var copyObject = function (obj) { + return JSON.parse(JSON.stringify(obj)); + }; + + // special UI elements + //var $title = $('#title').attr('placeholder', Messages.poll_titleHint || 'title'); TODO + var $description = $('#description').attr('placeholder', Messages.poll_descriptionHint || 'description'); + + var ready = function (info, userid, readOnly) { + console.log("READY"); + console.log('userid: %s', userid); + + var proxy = APP.proxy; + var uncommitted = APP.uncommitted = {}; + prepareProxy(proxy, copyObject(Render.Example)); + prepareProxy(uncommitted, copyObject(Render.Example)); + if (!readOnly && proxy.table.colsOrder.indexOf(userid) === -1 && + uncommitted.table.colsOrder.indexOf(userid) === -1) { + uncommitted.table.colsOrder.unshift(userid); + } + + var displayedObj = mergeUncommitted(proxy, uncommitted, false); + + var colsOrder = sortColumns(displayedObj.table.colsOrder, userid); + + var $table = APP.$table = $(Render.asHTML(displayedObj, null, colsOrder, readOnly)); + var $createRow = APP.$createRow = $('#create-option').click(function () { + console.error("BUTTON CLICKED! LOL"); + Render.createRow(proxy, function () { + change(); + }); + }); + + var $createCol = APP.$createCol = $('#create-user').click(function () { + Render.createColumn(proxy, function () { + change(); + }); + }); + + // Commit button + var $commit = APP.$commit = $('#commit').click(function () { + var uncommittedCopy = JSON.parse(JSON.stringify(APP.uncommitted)); + APP.uncommitted = {}; + prepareProxy(APP.uncommitted, copyObject(Render.Example)); + mergeUncommitted(proxy, uncommittedCopy, true); + APP.$commit.hide(); + change(); + }); + + // #publish button is removed in readonly + var $publish = APP.$publish = $('#publish') + .click(function () { + publish(true); + }); + + // #publish button is removed in readonly + var $admin = APP.$admin = $('#admin') + .click(function () { + publish(false); + }); + + // Title + if (APP.proxy.info.defaultTitle) { + updateDefaultTitle(APP.proxy.info.defaultTitle); + } else { + APP.proxy.info.defaultTitle = defaultName; + } + updateTitle(APP.proxy.info.title || defaultName); + + // Description + var resize = function () { + var lineCount = $description.val().split('\n').length; + $description.css('height', lineCount + 'rem'); + }; + $description.on('change keyup', function () { + var val = $description.val(); + proxy.info.description = val; + resize(); + }); + resize(); + if (typeof(proxy.info.description) !== 'undefined') { + $description.val(proxy.info.description); + } + + $('#tableScroll').prepend($table); + updateDisplayedTable(); + + $table + .click(handleClick) + .on('keyup', function (e) { handleClick(e, true); }); + + proxy + .on('change', ['info'], function (o, n, p) { + if (p[1] === 'title') { + updateTitle(n); + } else if (p[1] === "userData") { + addToUserData(APP.proxy.info.userData); + } else if (p[1] === 'description') { + var op = TextPatcher.diff(o, n); + var el = $description[0]; + + var selects = ['selectionStart', 'selectionEnd'].map(function (attr) { + var before = el[attr]; + var after = TextPatcher.transformCursor(el[attr], op); + return after; + }); + $description.val(n); + if (op) { + el.selectionStart = selects[0]; + el.selectionEnd = selects[1]; + } + } + + console.log("change: (%s, %s, [%s])", o, n, p.join(', ')); + }) + .on('change', ['table'], change) + .on('remove', [], change); + + addToUserData(APP.proxy.info.userData); + + getLastName(function (err, lastName) { + APP.ready = true; + + if (!proxy.published) { + publish(false); + } else { + publish(true); + } + + // Update the toolbar list: + // Add the current user in the metadata if he has edit rights + if (readOnly) { return; } + if (typeof(lastName) === 'string' && lastName.length) { + setName(lastName); + } else { + var myData = {}; + myData[info.myId] = { + name: "" + }; + addToUserData(myData); + APP.$userNameButton.click(); + } + }); + }; + + var create = function (info) { + var realtime = APP.realtime = info.realtime; + var myID = APP.myID = info.myID; + + var editHash; + var viewHash = Cryptpad.getViewHashFromKeys(info.channel, secret.keys); + + if (!readOnly) { + editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); + } + + APP.patchText = TextPatcher.create({ + realtime: realtime, + logging: true, + }); + + userList = APP.userList = info.userList; + var config = { + userData: userData, + readOnly: readOnly, + title: { + onRename: renameCb, + defaultName: defaultName, + suggestName: suggestName + }, + ifrw: window, + common: Cryptpad + }; + var toolbar = info.realtime.toolbar = Toolbar.create(APP.$bar, info.myID, info.realtime, info.getLag, userList, config); + + var $bar = APP.$bar; + var $rightside = $bar.find('.' + Toolbar.constants.rightside); + var $userBlock = $bar.find('.' + Toolbar.constants.username); + var $editShare = $bar.find('.' + Toolbar.constants.editShare); + var $viewShare = $bar.find('.' + Toolbar.constants.viewShare); + + // Store the object sent for the "change username" button so that we can update the field value correctly + var userNameButtonObject = APP.userName = {}; + /* add a "change username" button */ + getLastName(function (err, lastName) { + userNameButtonObject.lastName = lastName; + var $username = APP.$userNameButton = Cryptpad.createButton('username', false, userNameButtonObject, setName).hide(); + $userBlock.append($username); + }); + + /* add a forget button */ + var forgetCb = function (err, title) { + if (err) { return; } + document.title = title; + }; + var $forgetPad = Cryptpad.createButton('forget', true, {}, forgetCb); + $rightside.append($forgetPad); + + if (!readOnly) { + $editShare.append(Cryptpad.createButton('editshare', false, {editHash: editHash})); + } + if (viewHash) { + /* add a 'links' button */ + $viewShare.append(Cryptpad.createButton('viewshare', false, {viewHash: viewHash})); + if (!readOnly) { + $viewShare.append(Cryptpad.createButton('viewopen', false, {viewHash: viewHash})); + } + } + + // set the hash + if (!readOnly) { Cryptpad.replaceHash(editHash); } + + Cryptpad.getPadTitle(function (err, title) { + if (err) { + console.error(err); + console.log("Couldn't get pad title"); + return; + } + updateTitle(title || defaultName); + }); + }; + + var disconnect = function () { + //setEditable(false); // TODO + //Cryptpad.alert(Messages.common_connectionLost); // TODO + }; + + var config = { + websocketURL: Cryptpad.getWebsocketURL(), + channel: secret.channel, + readOnly: readOnly, + data: {}, + // our public key + validateKey: secret.keys.validateKey || undefined, + //readOnly: readOnly, + crypto: Crypto.createEncryptor(secret.keys), + }; + + // don't initialize until the store is ready. + Cryptpad.ready(function () { + if (readOnly) { + $('#commit, #create-user, #create-option, #publish, #admin').remove(); + } + + var parsedHash = Cryptpad.parsePadUrl(window.location.href); + defaultName = Cryptpad.getDefaultName(parsedHash); + var rt = window.rt = APP.rt = Listmap.create(config); + APP.proxy = rt.proxy; + rt.proxy + .on('create', create) + .on('ready', function (info) { + Cryptpad.getPadAttribute('userid', function (e, userid) { + if (e) { console.error(e); } + if (userid === null) { userid = Render.coluid(); } + APP.userid = userid; + Cryptpad.setPadAttribute('userid', userid, function (e) { + ready(info, userid, readOnly); + }); + }); + }) + .on('disconnect', disconnect); + + Cryptpad.getAttribute(HIDE_INTRODUCTION_TEXT, function (e, value) { + if (e) { console.error(e); } + if (value === null) { + Cryptpad.setAttribute(HIDE_INTRODUCTION_TEXT, "1", function (e) { + if (e) { console.error(e); } + }); + } else if (value === "1") { + $('#howItWorks').hide(); + } + }); + }); +}); + diff --git a/www/poll/test/render.js b/www/poll/test/render.js new file mode 100644 index 000000000..b0b41369d --- /dev/null +++ b/www/poll/test/render.js @@ -0,0 +1,446 @@ +define([ + '/common/cryptpad-common.js', + '/bower_components/hyperjson/hyperjson.js', + '/bower_components/textpatcher/TextPatcher.js', + '/bower_components/diff-dom/diffDOM.js', +], function (Cryptpad, Hyperjson, TextPatcher) { + var DiffDOM = window.diffDOM; + + var Example = { + info: { + title: '', + description: '', + userData: {} + }, + table: { +/* TODO + +deprecate the practice of storing cells, cols, and rows separately. + +Instead, keep everything in one map, and iterate over columns and rows +by maintaining indexes in rowsOrder and colsOrder + +*/ + cells: {}, + cols: {}, + colsOrder: [], + rows: {}, + rowsOrder: [] + } + }; + + var Render = { + Example: Example + }; + + var Uid = Render.Uid = function (prefix, f) { + f = f || function () { + return Number(Math.random() * Number.MAX_SAFE_INTEGER) + .toString(32).replace(/\./g, ''); + }; + return function () { return prefix + '-' + f(); }; + }; + + var coluid = Render.coluid = Uid('x'); + var rowuid = Render.rowuid = Uid('y'); + + var isRow = Render.isRow = function (id) { return /^y\-[^_]*$/.test(id); }; + var isColumn = Render.isColumn = function (id) { return /^x\-[^_]*$/.test(id); }; + var isCell = Render.isCell = function (id) { return /^x\-[^_]*_y\-.*$/.test(id); }; + + var typeofId = Render.typeofId = function (id) { + if (isRow(id)) { return 'row'; } + if (isColumn(id)) { return 'col'; } + if (isCell(id)) { return 'cell'; } + return null; + }; + + var getCoordinates = Render.getCoordinates = function (id) { + return id.split('_'); + }; + + var getColumnValue = Render.getColumnValue = function (obj, colId) { + return Cryptpad.find(obj, ['table', 'cols'].concat([colId])); + }; + + var getRowValue = Render.getRowValue = function (obj, rowId) { + return Cryptpad.find(obj, ['table', 'rows'].concat([rowId])); + }; + + var getCellValue = Render.getCellValue = function (obj, cellId) { + return Cryptpad.find(obj, ['table', 'cells'].concat([cellId])); + }; + + var setRowValue = Render.setRowValue = function (obj, rowId, value) { + var parent = Cryptpad.find(obj, ['table', 'rows']); + if (typeof(parent) === 'object') { return (parent[rowId] = value); } + return null; + }; + + var setColumnValue = Render.setColumnValue = function (obj, colId, value) { + var parent = Cryptpad.find(obj, ['table', 'cols']); + if (typeof(parent) === 'object') { return (parent[colId] = value); } + return null; + }; + + var setCellValue = Render.setCellValue = function (obj, cellId, value) { + var parent = Cryptpad.find(obj, ['table', 'cells']); + if (typeof(parent) === 'object') { return (parent[cellId] = value); } + return null; + }; + + var createColumn = Render.createColumn = function (obj, cb, id, value) { + var order = Cryptpad.find(obj, ['table', 'colsOrder']); + if (!order) { throw new Error("Uninitialized realtime object!"); } + id = id || coluid(); + value = value || ""; + setColumnValue(obj, id, value); + order.push(id); + if (typeof(cb) === 'function') { cb(void 0, id); } + }; + + var removeColumn = Render.removeColumn = function (obj, id, cb) { + var order = Cryptpad.find(obj, ['table', 'colsOrder']); + var parent = Cryptpad.find(obj, ['table', 'cols']); + + if (!(order && parent)) { throw new Error("Uninitialized realtime object!"); } + + var idx = order.indexOf(id); + if (idx === -1) { + return void console + .error(new Error("Attempted to remove id which does not exist")); + } + + Object.keys(obj.table.cells).forEach(function (key) { + if (key.indexOf(id) === 0) { + delete obj.table.cells[key]; + } + }); + + order.splice(idx, 1); + if (parent[id]) { delete parent[id]; } + if (typeof(cb) === 'function') { + cb(); + } + }; + + var createRow = Render.createRow = function (obj, cb, id, value) { + console.error('new row!'); + var order = Cryptpad.find(obj, ['table', 'rowsOrder']); + if (!order) { throw new Error("Uninitialized realtime object!"); } + id = id || rowuid(); + value = value || ""; + setRowValue(obj, id, value); + order.push(id); + if (typeof(cb) === 'function') { cb(void 0, id); } + }; + + var removeRow = Render.removeRow = function (obj, id, cb) { + var order = Cryptpad.find(obj, ['table', 'rowsOrder']); + var parent = Cryptpad.find(obj, ['table', 'rows']); + + if (!(order && parent)) { throw new Error("Uninitialized realtime object!"); } + + var idx = order.indexOf(id); + if (idx === -1) { + return void console + .error(new Error("Attempted to remove id which does not exist")); + } + + order.splice(idx, 1); + if (parent[id]) { delete parent[id]; } + if (typeof(cb) === 'function') { cb(); } + }; + + var setValue = Render.setValue = function (obj, id, value) { + var type = typeofId(id); + + switch (type) { + case 'row': return setRowValue(obj, id, value); + case 'col': return setColumnValue(obj, id, value); + case 'cell': return setCellValue(obj, id, value); + case null: break; + default: + console.log("[%s] has type [%s]", id, type); + throw new Error("Unexpected type!"); + } + }; + + var getValue = Render.getValue = function (obj, id) { + switch (typeofId(id)) { + case 'row': return getRowValue(obj, id); + case 'col': return getColumnValue(obj, id); + case 'cell': return getCellValue(obj, id); + case null: break; + default: throw new Error("Unexpected type!"); + } + }; + + var getRowIds = Render.getRowIds = function (obj) { + return Cryptpad.find(obj, ['table', 'rowsOrder']); + }; + + var getColIds = Render.getColIds = function (obj) { + return Cryptpad.find(obj, ['table', 'colsOrder']); + }; + + var getCells = Render.getCells = function (obj) { + return Cryptpad.find(obj, ['table', 'cells']); + }; + + /* cellMatrix takes a proxy object, and optionally an alternate ordering + of row/column keys (as an array). + + it returns an array of arrays containing the relevant data for each + cell in table we wish to construct. + */ + var cellMatrix = Render.cellMatrix = function (obj, rows, cols, readOnly) { + if (typeof(obj) !== 'object') { + throw new Error('expected realtime-proxy object'); + } + + var cells = getCells(obj); + rows = rows || getRowIds(obj); + rows.push(''); + cols = cols || getColIds(obj); + + return [null].concat(rows).map(function (row, i) { + if (i === 0) { + return [null].concat(cols.map(function (col) { + var result = { + 'data-rt-id': col, + type: 'text', + value: getColumnValue(obj, col) || "", + placeholder: Cryptpad.Messages.poll_userPlaceholder, + disabled: 'disabled' + }; + return result; + })); + } + if (i === rows.length) { + return [null].concat(cols.map(function (col) { + return { + 'class': 'lastRow', + }; + })); + } + + return [{ + 'data-rt-id': row, + value: getRowValue(obj, row), + type: 'text', + placeholder: Cryptpad.Messages.poll_optionPlaceholder, + disabled: 'disabled' + }].concat(cols.map(function (col) { + var id = [col, rows[i-1]].join('_'); + var val = cells[id] || false; + var result = { + 'data-rt-id': id, + type: 'checkbox', + autocomplete: 'nope', + }; + if (readOnly) { + result.disabled = "disabled"; + } + if (val) { result.checked = true; } + return result; + })); + }); + }; + + var makeRemoveElement = Render.makeRemoveElement = function (id) { + return ['SPAN', { + 'data-rt-id': id, + class: 'remove', + }, ['✖']]; + }; + + var makeEditElement = Render.makeEditElement = function (id) { + return ['SPAN', { + 'data-rt-id': id, + class: 'edit', + }, ['✐']]; + }; + + var makeLockElement = Render.makeLockElement = function (id) { + return ['SPAN', { + 'data-rt-id': id, + class: 'lock', + }, ['🔒']]; + }; + + var makeHeadingCell = Render.makeHeadingCell = function (cell, readOnly) { + if (!cell) { return ['TD', {}, []]; } + if (cell.type === 'text') { + var removeElement = makeRemoveElement(cell['data-rt-id']); + var editElement = makeEditElement(cell['data-rt-id']); + var lockElement = makeLockElement(cell['data-rt-id']); + var elements = [['INPUT', cell, []]]; + if (!readOnly) { + elements.unshift(removeElement); + elements.unshift(lockElement); + elements.unshift(editElement); + } + return ['TD', {}, elements]; + } + return ['TD', cell, []]; + }; + + var clone = function (o) { + return JSON.parse(JSON.stringify(o)); + }; + + var makeCheckbox = Render.makeCheckbox = function (cell) { + var attrs = clone(cell); + + // FIXME + attrs.id = cell['data-rt-id']; + + var labelClass = 'cover'; + if (cell.checked) { + labelClass += ' yes'; + } + + return ['TD', {class:"checkbox-cell"}, [ + ['DIV', {class: 'checkbox-contain'}, [ + ['INPUT', attrs, []], + ['SPAN', {class: labelClass}, []], + ['LABEL', { + for: attrs.id, + 'data-rt-id': attrs.id, + }, []] + ]] + ]]; + }; + + var makeBodyCell = Render.makeBodyCell = function (cell, readOnly) { + if (cell && cell.type === 'text') { + var removeElement = makeRemoveElement(cell['data-rt-id']); + var editElement = makeEditElement(cell['data-rt-id']); + var elements = [['INPUT', cell, []]]; + if (!readOnly) { + elements.push(removeElement); + elements.push(editElement); + } + return ['TD', {}, [ + ['DIV', {class: 'text-cell'}, elements] + ]]; + } + + if (cell && cell.type === 'checkbox') { + return makeCheckbox(cell); + } + return ['TD', cell, []]; + }; + + var makeBodyRow = Render.makeBodyRow = function (row, readOnly) { + return ['TR', {}, row.map(function (cell) { + return makeBodyCell(cell, readOnly); + })]; + }; + + var toHyperjson = Render.toHyperjson = function (matrix, readOnly) { + if (!matrix || !matrix.length) { return; } + var head = ['THEAD', {}, [ ['TR', {}, matrix[0].map(function (cell) { + return makeHeadingCell(cell, readOnly); + })] ]]; + var foot = ['TFOOT', {}, matrix.slice(-1).map(function (row) { + return makeBodyRow(row, readOnly); + })]; + var body = ['TBODY', {}, matrix.slice(1, -1).map(function (row) { + return makeBodyRow(row, readOnly); + })]; + return ['TABLE', {id:'table'}, [head, foot, body]]; + }; + + var asHTML = Render.asHTML = function (obj, rows, cols, readOnly) { + return Hyperjson.toDOM(toHyperjson(cellMatrix(obj, rows, cols, readOnly), readOnly)); + }; + + var diffIsInput = Render.diffIsInput = function (info) { + var nodeName = Cryptpad.find(info, ['node', 'nodeName']); + if (nodeName !== 'INPUT') { return; } + return true; + }; + + var getInputType = Render.getInputType = function (info) { + return Cryptpad.find(info, ['node', 'type']); + }; + + var preserveCursor = Render.preserveCursor = function (info) { + if (['modifyValue', 'modifyAttribute'].indexOf(info.diff.action) !== -1) { + var element = info.node; + + if (typeof(element.selectionStart) !== 'number') { return; } + + var o = info.oldValue || ''; + var n = info.newValue || ''; + var op = TextPatcher.diff(o, n); + + info.selection = ['selectionStart', 'selectionEnd'].map(function (attr) { + var before = element[attr]; + var after = TextPatcher.transformCursor(element[attr], op); + return after; + }); + } + }; + + var recoverCursor = Render.recoverCursor = function (info) { + try { + if (info.selection && info.node) { + info.node.selectionStart = info.selection[0]; + info.node.selectionEnd = info.selection[1]; + } + } catch (err) { + //console.log(info.node); + //console.error(err); + } + }; + + var diffOptions = { + preDiffApply: function (info) { + if (!diffIsInput(info)) { return; } + switch (getInputType(info)) { + case 'checkbox': + //console.log('checkbox'); + //console.log("[preDiffApply]", info); + break; + case 'text': + preserveCursor(info); + break; + default: break; + } + }, + postDiffApply: function (info) { + if (info.selection) { recoverCursor(info); } + /* + if (!diffIsInput(info)) { return; } + switch (getInputType(info)) { + case 'checkbox': + console.log("[postDiffApply]", info); + break; + case 'text': break; + default: break; + }*/ + } + }; + + var updateTable = Render.updateTable = function (table, obj, conf) { + var DD = new DiffDOM(diffOptions); + + var rows = conf ? conf.rows : null; + var cols = conf ? conf.cols : null; + var readOnly = conf ? conf.readOnly : false; + var matrix = cellMatrix(obj, rows, cols, readOnly); + + var hj = toHyperjson(matrix, readOnly); + + if (!hj) { throw new Error("Expected Hyperjson!"); } + + var table2 = Hyperjson.toDOM(hj); + var patch = DD.diff(table, table2); + DD.apply(table, patch); + }; + + return Render; +});