/* * Copyright (c) 2012-2013 Kaarle Ritvanen * See LICENSE file for license details */ $(function() { $("#login").submit(function() { var statusBar = (function() { function set(status, msg, mode) { $("#status").prop("class", status); $("#status p").text(msg); $("#logout").prop("class", mode ? "hidden" : null); $("#status div").prop("class", mode == "txn" ? null : "hidden"); $("#commit").prop("disabled", status == "invalid"); } function setError(msg, mode) { set("invalid", msg, mode); }; return { reset: function() { set(null, ""); }, setError: setError, validationReady: function(txnValid) { if (txnValid) set("changed", "You have uncommitted changes", "txn"); else setError("Some values need checking", "txn"); } } })(); $.ajax("/login", { type: "POST", data: JSON.stringify({ username: $("#username").val(), password: $("#password").val() }) }).done(function(data, status, xhr) { function split(path) { var res = []; while (path && path != "/") { var comp = path.match(/^\/([^\\\/]|\\.)+/)[0]; res.push(comp.substring(1)); path = path.substring(comp.length); } return res; } function join() { var arg = _.toArray(arguments); if (arg.length == 1) return arg[0]; var path = arg.shift(); var name = arg.shift(); if (_.isString(name)) { name = name.replace(/([\\\/])/g, "\\$1"); if (!isNaN(Number(name))) name = "\\" + name; } arg.unshift((path == "/" ? "" : path) + "/" + name); return join.apply(undefined, arg); } function isRealSubordinate(p1, p2) { return !p1.indexOf(p2 + "/"); } function isSubordinate(p1, p2) { return p1 == p2 || isRealSubordinate(p1, p2); } function isTreeNode(meta) { return _.contains( ["collection", "list", "model", "set"], meta.type ); } var txnMgr = (function(token, saveRequired) { var txn, changed, invalid; function reset() { txn = null; changed = {}; invalid = {}; } reset(); function isValid() { return !(_.size(invalid)); } function request(url, options) { options = options || {}; options.headers = {"X-ACF-Auth-Token": token}; if (txn) options.headers["X-ACF-Transaction-ID"] = txn; return $.ajax(url, options); } function abort() { request("/transaction", {type: "DELETE"}); reset(); } function objRequest(path, options) { return request("/config" + path, options); } function query(path) { var def = $.Deferred(); objRequest(path).done(function(data) { function index(name) { return _.isArray(data.data) ? name - 1 : name; } data.get = function(name, valid) { var p = join(path, name); return (!valid && p in invalid) ? invalid[p][0] : data.data[index(name)]; }; data.status = function(name) { var p = join(path, name); function scan(objs) { return _.size(_.filter( _.keys(objs), function(obj) { return isSubordinate(obj, p); } )); } if (scan(invalid)) return "invalid"; if (scan(changed)) return "changed"; return null; } data.set = function(name, newValue) { var def = $.Deferred(); function reject(xhr) { def.reject(xhr); } var mpath = join(path, name); var value = data.get(name, true); if (value == undefined) value = null; var tn = _.isObject(newValue); var npv = tn ? mpath : newValue; function resolve() { if (mpath in invalid && invalid[mpath][1] == def) { var del = invalid[mpath][0] == null; delete invalid[mpath]; if (del) _.each( _.keys(invalid), function(p) { if (isRealSubordinate(p, mpath)) delete invalid[p]; } ); } def.resolve(isValid()); } function validate() { var options; if (newValue != null) options = { type: "PUT", data: JSON.stringify(newValue) }; else if (data.get(name, true) != null) options = {type: "DELETE"}; if (!options) { if (data.meta.type == "model" && _.findWhere( data.meta.fields, {name: name} ).required) def.reject("Required value not set"); else resolve(); return; } objRequest(mpath, options).done(function() { if (!(mpath in changed)) changed[mpath] = value; if (!tn && newValue == changed[mpath]) delete changed[mpath]; if (npv == null) _.each( _.keys(changed), function(p) { if (isRealSubordinate(p, mpath)) delete changed[p]; } ); data.data[index(name)] = npv; if (tn) query(mpath).done(function(data) { if (mpath in invalid && data.meta.type == "model") _.each( data.meta.fields, function(field) { var mmpath = join( mpath, field.name ); if (field.required && !(mmpath in invalid) && data.get( field.name ) == null) invalid[mmpath] = [ null ]; }); resolve(); }).fail(reject); else resolve(); }).fail(reject); } var prevTask; if (mpath in invalid) prevTask = invalid[mpath][1]; invalid[mpath] = [npv, def]; if (prevTask) prevTask.always(validate); else validate(); return def; }; data.delete = function(name) { var def = $.Deferred(); function resolve(txnValid) { def.resolve(txnValid); } function reject() { def.reject(); } var tasks = _.filter( _.pluck(_.values(invalid), 1), function(d) { return d.state() == "pending"; } ); if (tasks.length) tasks[0].always(function() { data.delete(name).done(resolve).fail(reject); }); else { var length = data.data.length; data.set(name, null).done(function(txnValid) { if (isTreeNode(data.meta)) { delete changed[join(path, name)]; changed[path] = path; if (data.meta.type == "list") for (var i = name; i < length; i++) { var opath = join(path, i + 1); var npath = join(path, i); _.each( [changed, invalid], function(map) { _.each( _.keys(map), function(p) { if (isSubordinate( p, opath )) { map[npath + p.substring( opath.length )] = map[p]; delete map[p]; } }); } ); } } resolve(txnValid); }).fail(reject); } return def; }; def.resolve(data); }).fail(function() { def.reject(); }); return def; } return { start: function() { var def = $.Deferred(); if (txn && isValid() && !(_.size(changed))) abort(); if (txn) def.resolve(); else request("/transaction", {type: "POST"}) .done(function(data, status, xhr) { txn = xhr.getResponseHeader( "X-ACF-Transaction-ID" ); def.resolve(); }) .fail(function() { def.reject(); }); return def; }, commit: function() { var def = $.Deferred(); function reject(xhr) { def.reject(xhr); } request( "/transaction", {type: "PUT"} ).done(function() { reset(); if (saveRequired) request( "/save", {type: "POST"} ).done(function() { def.resolve(); }).fail(reject); else def.resolve(); }).fail(reject); return def; }, abort: abort, query: query, logout: function() { return request("/login", {type: "DELETE"}); } }; })( xhr.getResponseHeader("X-ACF-Auth-Token"), xhr.getResponseHeader("X-ACF-Save-Required") == "1" ); function formatError(msg, xhr) { msg += " " + xhr.statusCode().status; if (xhr.responseText) msg += ': ' + xhr.responseText; return msg; } function href() { return $("").attr({href: "javascript:void(0);"}); } var Widget = { init: function( data, name, meta, label, level, editable, removable ) { var value = data.get(name); if (!editable) { var el = this.staticRender(value, level); this.setElStatus(el, status); if (label) return this.attachLabel(el, label); return el; } this.el = this.render(value, meta, level); var el = this.attachLabel(this.wrap(), label); if (removable) { var link = href().click(function() { data.delete(name).done(function(txnValid) { statusBar.validationReady(txnValid) fetchAndRender($.param.fragment()); }) }).text("Delete"); if (el.is("tr")) link = $("").html(link); el.append(link); } this.setStatus(data.status(name)); return el; }, setElStatus: function(el, status) { el.prop("class", status); }, attachLabel: function(el, label) { var row = $(""); row.append($("").text(label)); row.append(el); return row; }, wrap: function() { return this.el; }, setStatus: function(status) { this.setElStatus(this.el, status); } } var Field = Object.create(Widget); Field.init = function( data, name, meta, label, level, editable, removable ) { var el = _.bind(Widget.init, this)( data, name, meta, label, level, editable, removable ); if (editable) { if (!this.field) this.field = this.el; var self = this; function change() { self.msg.text("[checking]"); statusBar.setError("Validating changes", "validate"); data.set(name, self.get()).done(function(txnValid) { self.msg.empty() self.setStatus(data.status(name)); statusBar.validationReady(txnValid); }).fail(function(xhr) { if (_.isString(xhr)) self.msg.text(xhr); else if (xhr.statusCode().status == 422) self.msg.html( _.reduce( _.map( $.parseJSON(xhr.responseText), _.escape ), function(a, b) { return a + "
" + b; } ) ); else self.msg.text(formatError("Error", xhr)); self.setStatus("invalid"); statusBar.validationReady(false); }); } this.field.change(change); if (data.status(name) == "invalid") change(); } return el; }; Field.staticRender = function(value, level) { return $("
").text(value); }; Field.render = function(value, meta, level) { return $("").attr({type: "text", value: value}); }; Field.wrap = function() { var td = $(""); this.msg = $("
"); td.append(this.msg); td.append(this.el); return td; }; Field.get = function() { return this.field.val() || null; }; var ComboBox = Object.create(Field); ComboBox.render = function(value, meta, level) { var el = $("").attr({type: "checkbox", checked: value}); }; CheckBox.get = function() { return this.el.is(":checked"); }; var Link = Object.create(Widget); Link.staticRender = function(value, level) { var el = href(); if (value) { el.click(function() { $.bbq.pushState("#" + value); }).text("Show"); } return el; }; Link.render = function(value, meta, level) { return this.staticRender(value, level); }; Link.get = function() { return {}; }; Inline = Object.create(Link); Inline.staticRender = function(value, level) { var el = $("
"); var obj = $("
"); fetchAndRender(value, obj, level == 6 ? 6 : level + 1); el.append(obj); return el; }; Inline.attachLabel = function(el, label) { return el; }; Inline.setStatus = function(status) {}; var Reference = Object.create(ComboBox); Reference.staticRender = function(value, level) { return Link.staticRender(value, level).text(value); }; Reference.setElStatus = function(el, status) { ComboBox.setElStatus(el.find("select"), status); }; Reference.init = function( data, name, meta, label, level, editable, removable ) { this.field = ComboBox.render(data.get(name), meta, level); return _.bind(ComboBox.init, this)( data, name, meta, label, level, editable, removable ); } Reference.render = function(value, meta, level) { var el = $("
").html(this.field); el.append(" "); var link = $("
"); var update = _.bind(function() { link.html(Link.staticRender(this.get(), level)); }, this); el.append(link); this.field.change(update); update(); return el; }; var widgets = { boolean: CheckBox, combobox: ComboBox, field: Field, inline: Inline, link: Link, reference: Reference } function renderObject(path, data, target, level) { target = target || $("#content"); level = level || 1; target.html($("").text(data.meta["ui-name"])); if (!isTreeNode(data.meta)) return target.append(JSON.stringify(data)); var div = $("
"); target.append(div); var table; function appendRow(row) { if (!table) { table = $(""); div.append(table); } table.append(row); } function renderField(name, meta, label, editable, removable) { var el = Object.create(widgets[meta.widget]).init( data, name, meta, label, level, editable, removable ); if (el.is("tr")) appendRow(el); else { table = null; div.append(el); } } function renderCollectionMember(name, meta) { var set = meta.type == "set"; renderField( name, meta.members, meta["ui-member"] + " " + name, !set, !set ); } if (data.meta.type == "model") _.each(data.meta.fields, function(field) { renderField( field.name, field, field["ui-name"], true, false ); }); else _.each(data.data, function(value, name) { if (_.isArray(data.data)) name++; renderCollectionMember(name, data.meta); }); if (_.contains(["collection", "list"], data.meta.type)) { var keys = _.clone(_.keys(data.data)); var button = $("").attr( {type: "submit", value: "Insert"} ).click(function() { var getter; function insert() { var name = getter(); if (_.contains(keys, name)) { button.prop("class", null); return; } keys.push(name); data.set( name, isTreeNode(data.meta.members) ? {} : null ).done(function(txnValid) { renderCollectionMember(name, data.meta); button.prop("class", null); statusBar.validationReady(txnValid); }); } button.prop("class", "hidden"); if (data.meta.type == "collection") { var field = $("").attr({type: "text"}); var row = $("").html($("
").html(field)); getter = function() { var res = field.val(); row.remove(); return res; } field.change(insert); appendRow(row); } else { getter = function() { return data.data.length + 1; }; insert(); } }); target.append($("

").html(button)); } } function fetchAndRender(path, target, level) { var def = $.Deferred(); txnMgr.query(path).done(function(data) { renderObject(path, data, target, level); def.resolve(); }).fail(function() { def.reject(); }); return def; } function redirect(path) { $.bbq.pushState("#" + path); } function render() { var path = $.param.fragment(); function renderMenu(target, path, current, selectFirst) { var def = $.Deferred(); txnMgr.query(path).done(function(data) { if (data.meta.type != "model" || _.filter( data.meta.fields, function(field) { return !isTreeNode(field); } ).length) { def.reject(data); return; } var first = data.meta.fields[0].name; if (!current && selectFirst) current = first; _.each(data.meta.fields, function(field) { var el = $("

  • "); var link = Link.staticRender(data.get(field.name)); link.text(field["ui-name"]); el.prop("class", data.status(field.name)); if (field.name == current) el.addClass("current"); el.append(link); target.append(el); }); def.resolve(first); }); return def; } txnMgr.start().done(function() { var comps = split(path); renderMenu($("#modules").empty(), "/", comps[0], false); var tabs = $("#tabs").empty(); if (path == "/") return; var topLevel = comps.length == 1; renderMenu(tabs, "/" + comps[0], comps[1], true) .done(function(first) { fetchAndRender( topLevel ? join(path, first) : path ).fail(function() { comps.pop(); comps.unshift("/"); redirect(join.apply(undefined, comps)); }); }) .fail(function(data) { if (topLevel) renderObject(path, data); else fetchAndRender(path); }); }); } function clearState() { statusBar.reset(); render(); } $("#commit").click(function() { txnMgr.commit().done(clearState).fail(function(xhr) { statusBar.setError( formatError("Commit failed", xhr), "txn" ); }) }); $("#revert").click(function() { txnMgr.abort(); clearState(); }); $("#logout").click(function() { txnMgr.logout().done(function() { $("body").html($("

    ").text("Logged out")); }); }); statusBar.reset(); $("#content").empty(); $(window).bind("hashchange", render); redirect("/"); }).fail(function() { statusBar.setError("Login failed", "login"); }); return false; }); $("#username").focus(); });