diff options
author | Kaarle Ritvanen <kaarle.ritvanen@datakunkku.fi> | 2013-05-17 22:15:00 +0300 |
---|---|---|
committer | Kaarle Ritvanen <kaarle.ritvanen@datakunkku.fi> | 2013-05-17 22:15:00 +0300 |
commit | 798bd31d507d13d27e92d576a09dafb96cb49453 (patch) | |
tree | 4d554c0f10fdb6386023809700e1cfd6c722234d /web | |
parent | 983b8d436c6449569243c4edb0268df0add8df28 (diff) | |
download | aconf-798bd31d507d13d27e92d576a09dafb96cb49453.tar.bz2 aconf-798bd31d507d13d27e92d576a09dafb96cb49453.tar.xz |
web client: basic editing functionality
Diffstat (limited to 'web')
-rw-r--r-- | web/client.html | 1 | ||||
-rw-r--r-- | web/client.js | 309 |
2 files changed, 267 insertions, 43 deletions
diff --git a/web/client.html b/web/client.html index 6417715..55ff445 100644 --- a/web/client.html +++ b/web/client.html @@ -14,6 +14,7 @@ <script type="text/javascript" src="client.js"></script> </head> <body> + <p id="status"></p> <div id="content"></div> </body> </html> diff --git a/web/client.js b/web/client.js index 3a6a277..c788984 100644 --- a/web/client.js +++ b/web/client.js @@ -11,61 +11,284 @@ $(function() { var token = xhr.getResponseHeader("X-ACF-Auth-Token"); - $(window).bind("hashchange", function() { + function request(url, txn, options) { + options = options || {}; + options.headers = {"X-ACF-Auth-Token": token}; + if (txn) options.headers["X-ACF-Transaction-ID"] = txn; + return $.ajax(url, options); + } + + var txn, changed, invalid; + + function startTxn() { + var def = $.Deferred(); + request("/", null, {type: "POST"}) + .done(function(data, status, xhr) { + txn = xhr.getResponseHeader("X-ACF-Transaction-ID"); + changed = {}; + invalid = {}; + $("#status").empty(); + def.resolve(); + }); + return def; + } + + + var Field = { + format: function(value) { return $("<div>").text(value); }, + init: function(value, meta) { + this.el = $("<input>").attr({type: "text", value: value}); + }, + get: function() { return this.el.val() || null; } + } + + var ComboBox = Object.create(Field); + ComboBox.init = function(value, meta) { + var el = $("<select>"); + this.el = el; + + function opt(value, ui_value, selected) { + el.append($("<option>").attr( + {value: value, selected: selected} + ).text(ui_value)); + } + + if (!meta.required) + opt("", "(none)", value == null) + + _.each( + _.zip(meta.choice, meta["ui-choice"]), + function(choice) { + opt(choice[0], choice[1], value == choice[0]); + } + ); + } + + var Path = { + format: function(value) { + var el = $("<a>"); + if (value) { + el.attr({href: "javascript:void(0);"}).click(function() { + $.bbq.pushState("#" + value); + }).text(value); + } + return el; + }, + init: function(value, meta) { this.el = this.format(value); } + } + + widgets = { + boolean: { + format: function(value) { return value ? "Yes" : "No"; }, + init: function(value, meta) { + this.el = $("<input>").attr( + {type: "checkbox", checked: value} + ); + }, + get: function() { return this.el.is(":checked"); } + }, + collection: Path, + combobox: ComboBox, + field: Field, + model: Path, + reference: { + format: Path.format, + init: function(value, meta) { + this.cbox = Object.create(ComboBox) + this.cbox.init(value, meta); + + var link = $("<div>"); + var update = _.bind(function() { + link.html(this.format(this.get())); + }, this); + this.cbox.el.change(update); + update(); + + this.el = $("<div>"); + this.el.append(this.cbox.el); + this.el.append(" "); + this.el.append(link); + }, + get: function() { return this.cbox.get(); } + } + } + + + function render() { var path = $.param.fragment(); + var url = "/config" + path; - $.ajax("/config" + path, {headers: {"X-ACF-Auth-Token": token}}) - .done(function(data) { - var content = $("#content"); - content.html($("<h1>").text(path)); + request(url, txn).done(function(data) { + var content = $("#content"); + content.html($("<h1>").text(path)); + + if (!_.contains([ + "collection", "model", "set" + ], data.meta.type)) { + content.append(JSON.stringify(data)); + return; + } - if (["collection", "model", "set"] - .indexOf(data.meta.type) == -1) { - content.append(JSON.stringify(data)); - return; - } + var table = $("<table>"); + + function renderField(url, name, value, meta, editable) { + var row = $("<tr>"); + if (name != null) row.append($("<td>").text(name)); + + var td = $("<td>"); + + var msg = $("<div>"); + if (url in changed) msg.text("[changed]"); + td.html(msg); - var table = $("<table>"); + if (meta.widget in widgets) { + var widget = widgets[meta.widget]; + var el; - function render(name, value, meta) { - var row = $("<tr>"); - if (name != null) row.append($("<td>").text(name)); + if (editable) { + var widget = Object.create(widget); + widget.init( + url in invalid ? invalid[url] : value, meta + ); + el = widget.el; + el.change(function() { + var options; + var newValue = widget.get(); + if (newValue == null) + options = {type: "DELETE"} + else options = { + type: "PUT", + data: JSON.stringify(newValue), + statusCode: { + 422: function(xhr) { + msg.html(_.reduce( + _.map( + $.parseJSON( + xhr.responseText + ), + _.escape + ), + function(a, b) { + return a + "<br/>" + b; + } + )); + } + } + } + var task = request(url, txn, options); + + if (!(url in changed)) changed[url] = value; + if (newValue == changed[url]) + delete changed[url]; + invalid[url] = newValue; + + msg.text("[checking]"); + + function showError(el, msg, xhr) { + msg += " " + xhr.statusCode().status; + if (xhr.responseText) + msg += ': ' + xhr.responseText; + el.text(msg); + } + + function newTxn() { + return startTxn().done(render); + } + + function abortTxn() { + request("/", txn, {type: "DELETE"}); + return newTxn(); + } + + var statusBar = $("#status"); + statusBar.html( + "You have uncommitted changes.<br/>" + ); + statusBar.append($("<input>").attr({ + id: "commit", + type: "submit", + value: "Commit", + disabled: true + }).click(function() { + request("/", txn, {type: "PUT"}) + .done(newTxn) + .fail(function(xhr) { + abortTxn().done(function() { + showError( + statusBar, + "Commit failed", + xhr + ); + }); + }); + })); + statusBar.append($("<input>").attr({ + type: "submit", value: "Revert" + }).click(abortTxn)); - var td = $("<td>"); - if (["collection", "model", "reference", "set"] - .indexOf(meta.type) > -1) { - - var link = $("<a>") - .attr({href: "javascript:void(0);"}) - .click(function() { - $.bbq.pushState("#" + value); + task.done(function() { + if (url in changed) msg.text("[changed]"); + else msg.empty(); + + delete invalid[url]; + if (!(_.size(invalid))) + $("#commit").prop("disabled", false); + + }).fail(function(xhr) { + showError(msg, "Error", xhr); }); - link.text(value); - td.html(link); + }); + + if (url in invalid) el.trigger("change"); } - else td.text(value); - row.append(td); - - row.append($("<td>").text(JSON.stringify(meta))); - - table.append(row); - } - if (data.meta.type == "model") - _.each(data.meta.fields, function(field) { - render( - field["ui-name"], data.data[field.name], field - ); - }); + else el = widget.format(value); - else _.each(data.data, function(value) { - render(null, value, data.meta.members); - }); + td.append(el); + } + + else td.text(value); + + row.append(td); + row.append($("<td>").text(JSON.stringify(meta))); + table.append(row); + } + + if (data.meta.type == "model") + _.each(data.meta.fields, function(field) { + var name = field.name; + if (_.isString(name)) { + name = name.replace(/([\\\/])/g, "\\$1"); + if (!isNaN(Number(name))) name = "\\" + name; + } - content.append(table); + renderField( + url + "/" + name, + field["ui-name"], + data.data[field.name], + field, + true + ); + }); + + else _.each(data.data, function(value) { + var i = 1; + renderField( + url + "/" + i++, + null, + value, + data.meta.members, + data.meta.type != "set" + ); }); - }); + + content.append(table); + }); + } - $.bbq.pushState("#/"); + startTxn().done(function() { + $(window).bind("hashchange", render); + $.bbq.pushState("#/"); + }); }) }) |