/* * Copyright (c) 2012-2014 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 escape(name) { if (!_.isString(name)) return String(name); name = name.replace(/([\\\/])/g, "\\$1"); if (isNaN(Number(name))) return name; return "\\" + name; } function join() { var arg = _.toArray(arguments); if (arg.length == 1) return arg[0]; var path = arg.shift(); var name = arg.shift(); arg.unshift((path == "/" ? "" : path) + "/" + escape(name)); return join.apply(undefined, arg); } function isSubordinate(p1, p2, real) { p1 = split(p1); p2 = split(p2); if (real && p1.length <= p2.length) return false; for (var i = 0; i < p2.length; i++) if (p1[i] != p2[i]) return false; return true; } function isList(meta) { return _.contains(["list", "set"], meta.type); } function isCollection(meta) { return meta.type == "collection" || isList(meta); } function isTreeNode(meta) { return meta.type == "model" || isCollection(meta); } 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; if (options.data != undefined) options.data = JSON.stringify(options.data); return $.ajax(url, options); } function abort() { request("/transaction", {type: "DELETE"}); reset(); } function objRequest(path, options) { return request("/config" + path, options); } function exclusive(task) { $.blockUI(); var def = $.Deferred(); function resolve(txnValid) { def.resolve(txnValid); } function reject() { def.reject(); } var tasks = _.filter( _.pluck(_.values(invalid), 1), function(d) { return d && d.state() == "pending"; } ); if (tasks.length) tasks[0].always(function() { exclusive(task).done(resolve).fail(reject); }); else task().always($.unblockUI).done(resolve).fail(reject); return def; } function query(path) { var def = $.Deferred(); objRequest(path).done(function(data) { if (isTreeNode(data.meta) && !_.size(data.data)) data.data = isList(data.meta) ? [] : {}; function index(name) { return _.isArray(data.data) ? name - 1 : name; } data.get = function(name, valid) { var p = join(path, name); if (!valid && p in invalid) return invalid[p][0]; if (data.meta.type == "set") { return _.contains( data.data, name ) ? name : null; } return data.data[index(name)]; }; data.metaRequest = function(name) { return request("/meta" + join(path, name)); }; data.match = function(filter) { if (!filter) return true; return _.every(_.map(filter, function(values, key) { return _.contains(values, data.data[key]); })); }; data.status = function(name) { var p = name ? join(path, name) : path; 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.validate = function() { var valid = true; if (data.meta.required) { valid = _.size(data.data); if (valid) delete invalid[path]; else invalid[path] = [path]; } return valid && isValid(); }; 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 ignore(path) { _.each(_.keys(invalid), function(p) { if (isSubordinate(p, path)) delete invalid[p]; }); } function resolve() { if (mpath in invalid && invalid[mpath][1] == def) { var del = invalid[mpath][0] == null; delete invalid[mpath]; if (del) ignore(mpath); } def.resolve(isValid()); } function validate() { var del = newValue == null; var set = data.meta.type == "set"; var options; if (!del) options = { type: set ? "POST" : "PUT", data: 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( set && !del ? path : 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 (isSubordinate(p, mpath, true)) delete changed[p]; } ); if (data.meta.type == "list" && del) data.data.splice(name - 1, 1); else if (!set) data.data[index(name)] = npv; else if (del) data.data.splice( data.data.indexOf(name), 1 ); else data.data.push(name); data.validate(); if (data.meta.type == "model") _.each( data.meta.fields, function(field) { if (field.condition && field.condition[name] && !_.contains( field.condition[name], newValue )) ignore( join(path, field.name) ); } ); if (tn && !set) query(mpath).done(function(data) { if (mpath in invalid) { if (data.meta.type == "model") _.each( data.meta.fields, function(field) { var mmpath = join( mpath, field.name ); if (field.required && data.match( field.condition ) && !(mmpath in invalid) && (isCollection( field ) || data.get( field.name ) == null)) { invalid[ mmpath ] = [ null ]; } }); else if (isCollection(data.meta) && data.meta.required) invalid[mpath] = [mpath]; } 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.add = function(name) { return data.set(name, name); }; function adjustListIndex(oldIndex, newIndex) { var opath = join(path, oldIndex); var npath = join(path, newIndex); _.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]; } }); } ); } function adjustListIndices(start, end) { var offset = start < end ? 1 : -1; for (var i = start; i != end; i += offset) adjustListIndex(i + offset, i); } function _delete(name) { var def = $.Deferred(); var length = data.data.length; data.set(name, null).done(function(txnValid) { if (isTreeNode(data.meta) && data.meta.type != "set") { delete changed[join(path, name)]; changed[path] = path; if (data.meta.type == "list") adjustListIndices(name, length); } def.resolve(txnValid); }).fail(function() { def.reject(); }); return def; } data.delete = function(name) { return exclusive(function() { return _delete(name); }); }; data.move = function(oldIndex, newIndex) { if (oldIndex == newIndex) return $.Deferred().resolve(isValid()); var value = data.get(oldIndex); var length = data.data.length; return exclusive(function() { var def = $.Deferred(); function reject() { def.reject(); } if (oldIndex > newIndex) oldIndex++; else newIndex++; objRequest(path, {type: "POST", data: { index: newIndex, data: isTreeNode(data.meta.members) ? join(path, oldIndex) : value }}).done(function() { data.data.splice(newIndex - 1, 0, value); adjustListIndices(length + 1, newIndex); adjustListIndex(oldIndex, newIndex); data.delete(oldIndex) .done(function(txnValid) { def.resolve(txnValid); }) .fail(reject); }).fail(reject); return def; }); }; data.invoke = function(name) { return objRequest(join(path, name), {type: "POST"}); }; 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);"}); } function objectRef(value, el) { el = el || href(); if (value) { el.click(function() { $.bbq.pushState("#" + value); }).text("Show"); } return el; }; function makeRow(el) { if (el.is("td")) return $("").html(el); return el; } function setStatus(el, status) { el.prop("class", status); } var Widget = { extend: function(spec) { var res = Object.create(this); for (key in spec) res[key] = spec[key]; res.base = this; return res; }, super: function() { var args = _.toArray(arguments); var cls = args.shift(); var key = args.shift(); return cls.base[key].apply(this, args); }, new: function(data, name, meta, level, editable, removable) { return Object.create(this).init( data, name, meta, level, editable, removable ); }, init: function(data, name, meta, level, editable, removable) { this.data = data; this.name = name; this.meta = meta; this.level = level; var value = data.get(name); var status = data.status(name); if (!editable || !meta.editable) { var el = this.staticRender(value, meta); if (el) { setStatus(el, status); return el; } } this.makeEl(); this.dynamic = meta.dynamic; var self = this; var request; function handleRequest(req) { request = req; req.done(function(value, meta) { if (req != request) return; self.render(value, meta); self.setStatus(status); }); } this.wrapped = this.wrap(); this.visible = true; if (removable) { var link = href().click(function() { data.delete(name).done(function(txnValid) { statusBar.validationReady(txnValid) renderObject(); }) }).text("Delete"); this.wrapped = makeRow(this.wrapped); if (this.wrapped.is("tr")) link = $("").html(link); this.wrapped.append(link); } handleRequest(this.requestData(value, meta)); function validate() { request.done(function(value) { self.validate(value); }); } this.wrapped.on("start", function(event) { if (data.status(name) == "invalid") validate(); else self.setVisible(); event.stopPropagation(); }); this.wrapped.on("updated", function(event, field) { if (self.dynamic) handleRequest(self.refreshData()); if (!field || self.dynamic || (meta.condition && field in meta.condition)) validate(); event.stopPropagation(); }); return this.wrapped; }, makeEl: function() { this.el = this.createEl(); }, requestData: function(value, meta) { return $.Deferred().resolve(value, meta); }, refreshData: function() { var def = $.Deferred(); var self = this; this.data.metaRequest(this.name).done(function(data) { def.resolve(self.get(), data); }); return def; }, wrap: function() { return this.el; }, showStatus: true, setStatus: function(status) { if (this.el && this.showStatus) setStatus(this.statusEl(), status); }, statusEl: function() { return this.el; }, setVisible: function() { this.visible = this.data.match(this.meta.condition); if (this.wrapped) this.wrapped.trigger("setVisible", [this.visible]); }, validate: function(value) { this.setVisible(); } }; var Field = Widget.extend({ init: function( data, name, meta, level, editable, removable ) { this.editable = editable && meta.editable; var el = this.super( Field, "init", data, name, meta, level, editable, removable ); if (this.editable) this.field.change(_.bind(this.validateChange, this)); return el; }, staticRender: function(value, meta) { return $("").text(value); }, makeEl: function() { this.super(Field, "makeEl"); if (!this.field) this.field = this.el; }, createEl: function() { return $(""); }, render: function(value, meta) { this.field.attr({type: "text", value: value}); }, wrap: function() { var td = $(""); this.msg = $("
"); td.append(this.msg); td.append(this.el); return td; }, validate: function(value) { this.super(Field, "validate", value); if (!this.visible || !this.editable) return; this.validateChange(); }, validateChange: function() { this.msg.text("[checking]"); statusBar.setError("Validating changes", "validate"); var self = this; this.data.set(this.name, this.get()) .done(function(txnValid) { self.msg.empty() self.setStatus(self.data.status(self.name)); statusBar.validationReady(txnValid); self.el.trigger("validated"); }) .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); }); }, get: function() { return this.editable ? ( this.field.val() || null ) : this.data.get(this.name); } }); var ComboBox = Field.extend({ createEl: function() { return $("").attr( {type: "submit", value: action["ui-name"]} ).click(function() { data.invoke(action.name).done(function() { alert("Done"); }).fail(function() { alert("Failed"); }); })); }); this.renderFields(data, meta); if (meta.editable && _.contains(["collection", "list"], meta.type)) { if (!this.table) this.makeSortable(this.div); 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(meta.members) ? {} : null ).done(function(txnValid) { if (_.isObject(meta.removable)) meta.removable = []; meta.removable.push(name); self.renderCollectionMember(name, meta); button.prop("class", null); statusBar.validationReady(txnValid); }); } button.prop("class", "hidden"); if (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); self.appendRow(row); } else { getter = function() { return data.data.length + 1; }; insert(); } }); this.el.append($("

").html(button)); } }, appendWidget: function(el, label) { var self = this; el = makeRow(el); if (el.is("tr")) { el.prepend($("").text(label)); this.appendRow(el); } else { if (el.is("table")) { this.makeSortable(el.find("tbody")); var td; el.find("tr").each(function(index, row) { td = $(""); $(row).prepend(td); }); td.text(label); this.table = el; } else this.table = null; this.div.append(el); } return el; }, appendRow: function(row) { if (!this.table) { this.table = this.makeSortable($("")); this.div.append($("").html(this.table)); } this.table.append(row); }, makeSortable: function(el) { var data = this.reqData; if (data.meta.type == "list") el.sortable({ start: function(event, ui) { $(":focus").change(); ui.item.data("index", ui.item.index()); }, stop: function(event, ui) { var oldIndex = ui.item.data("index") + 1; var newIndex = ui.item.index() + 1; if (newIndex != oldIndex) data.move(oldIndex, newIndex) .done(function(txnValid) { statusBar.validationReady(txnValid); renderObject(); }); } }); return el; } }); var CheckBoxes = HeaderInline.extend({ showStatus: true, setStatus: function(status) { this.super( CheckBoxes, "setStatus", status == "invalid" ? "invalid" : null ); }, render: function(data, meta) { this.dynamic = meta.members.dynamic; this.super(CheckBoxes, "render", data, meta); var table = $(""); this.el.append($("
").html(table)); var self = this; _.each(meta.members.choice, function(choice) { var selected = _.contains(data.data, choice.value); if (!(choice.enabled || selected)) return; var cbox = $("").attr({ type: "checkbox", checked: selected }); var row = $(""); row.append($("
").html(cbox)); var item = $(""); if (choice.ref) item.html(objectRef(choice.ref) .text(choice["ui-value"])); else item.text(choice["ui-value"]); row.append(item); function setRowStatus() { setStatus(row, data.status(choice.value)); } setRowStatus(); cbox.change(function() { ( cbox.is(":checked") ? data.add(choice.value) : data.delete(choice.value) ).done(function(txnValid) { self.setStatus(data.status()); setRowStatus(); statusBar.validationReady(txnValid); }); }); table.append(row); }); } }); var Reference = ComboBox.extend({ init: function( data, name, meta, level, editable, removable ) { this.field = ComboBox.createEl(); return this.super( Reference, "init", data, name, meta, level, editable, removable ); }, staticRender: function(value, meta) { return $("").html(objectRef(value).text(value)); }, statusEl: function() { return this.el.find("select"); }, createEl: function() { return $("
"); }, render: function(value, meta) { this.super(Reference, "render", value, meta); this.el.html(this.field); this.el.append(" "); var link = $("
"); var update = _.bind(function() { link.html(objectRef(this.get())); }, this); this.el.append(link); this.field.change(update); update(); } }); var widgets = { boolean: CheckBox, checkboxes: CheckBoxes, combobox: ComboBox, field: Field, inline: Vertical, link: Link, reference: Reference }; var Tabular = Vertical.extend({ render: function(data, meta) { this.header = true; this.super(Tabular, "render", data, meta); }, widget: function(meta) { if (!isTreeNode(meta)) return this.super(Tabular, "widget", meta); if (!this.header) return Horizontal; this.header = false; return HeaderHorizontal; } }); var Stacked = Vertical.extend({ widget: function(meta) { return isTreeNode(meta) ? Vertical : this.super(Stacked, "widget", meta); } }); var layouts = {stacked: Stacked, tabular: Tabular}; function redirect(path) { $.bbq.pushState("#" + path); } function renderObject(path, data) { path = path || $.param.fragment(); return ( data ? $.Deferred().resolve(data) : txnMgr.query(path) ).done(function(data) { var layout = data.meta.widget; var name = split(path).pop(); (layout ? layouts[layout] : Vertical).extend({ createEl: function() { return $("#content").empty(); }, wrap: function() { return this.el; }, requestData: function(value, meta) { return $.Deferred().resolve(data, data.meta); } }).new( { get: function(name) { return path; }, status: function(name) { return null; }, match: function(filter) { return true; } }, name, {}, 0, true, false ); }).fail(function() { var comps = split(path); comps.pop(); comps.unshift("/"); redirect(join.apply(undefined, comps)); }); }; 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") { def.reject(data); return; } var tnFields = []; var extraFields = false; _.each(data.meta.fields, function(field) { if (!field.visible) return; if (!isTreeNode(field)) extraFields = true; else if (data.data[field.name]) tnFields.push(field); }); if (!tnFields.length) { def.reject(data); return; } function addItem(path, ui_name, status, current) { var el = $("
  • ").html(objectRef(path).text(ui_name)); el.prop("class", status); if (current) el.addClass("current"); target.append(el); } if (extraFields) { addItem(path, "General", null, !current); selectFirst = false; } _.each(tnFields, function(field, i) { addItem( data.get(field.name), field["ui-name"], data.status(field.name), current == field.name || ( !current && !i && selectFirst ) ); field.visible = false; }); def.resolve(extraFields ? data : tnFields[0].name); }); return def; } if (path > "/") $("#content").text("Loading..."); txnMgr.start().done(function() { var comps = split(path); renderMenu($("#modules").empty(), "/", comps[0], false); var tabs = $("#tabs").empty(); if (path == "/") return; function renderTabs(p) { p = join(p, comps.shift()); renderMenu(tabs, p, comps[0], true) .done(function(data) { var tabLevel = !comps.length; renderObject( ( tabLevel && _.isString(data) ) ? join(p, data) : path, tabLevel && _.isObject(data) ? data : null ); }) .fail(function(data) { if (comps.length) renderTabs(p); else renderObject(p, data); }); } renderTabs("/"); }); } function clearState() { statusBar.reset(); render(); $.unblockUI(); } $("#commit").click(function() { $.blockUI(); txnMgr.commit().done(clearState).fail(function(xhr) { statusBar.setError( formatError("Commit failed", xhr), "txn" ); }) }); $("#revert").click(function() { $.blockUI(); 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(); });