summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKaarle Ritvanen <kaarle.ritvanen@datakunkku.fi>2014-02-06 23:09:13 +0200
committerKaarle Ritvanen <kaarle.ritvanen@datakunkku.fi>2014-02-14 22:35:56 +0200
commit38ac196b2f00c1e13a0928cd892384bc296c8a98 (patch)
tree473663650b5b4fd898bd9bab4c666dca6b900dd4
parent7d434464a843a8ca8acc903367492186a0ae13e8 (diff)
downloadacf2-38ac196b2f00c1e13a0928cd892384bc296c8a98.tar.bz2
acf2-38ac196b2f00c1e13a0928cd892384bc296c8a98.tar.xz
web client: extract each widget to a dedicated file
-rw-r--r--acf2/model/field.lua2
-rw-r--r--web/client.js793
-rw-r--r--web/layout/stacked.js17
-rw-r--r--web/layout/tabular.js31
-rw-r--r--web/widget/abstract/base.js134
-rw-r--r--web/widget/abstract/fields.js106
-rw-r--r--web/widget/abstract/inline.js74
-rw-r--r--web/widget/checkbox.js20
-rw-r--r--web/widget/checkboxes.js78
-rw-r--r--web/widget/combobox.js42
-rw-r--r--web/widget/field.js109
-rw-r--r--web/widget/inline.js155
-rw-r--r--web/widget/link.js23
-rw-r--r--web/widget/reference.js53
-rw-r--r--web/widget/table/header.js37
-rw-r--r--web/widget/table/row.js27
16 files changed, 915 insertions, 786 deletions
diff --git a/acf2/model/field.lua b/acf2/model/field.lua
index c5e36f2..a610b29 100644
--- a/acf2/model/field.lua
+++ b/acf2/model/field.lua
@@ -271,7 +271,7 @@ M.Boolean = class(Primitive)
function M.Boolean:init(params)
super(self, M.Boolean):init(params)
self.dtype = 'boolean'
- self.widget = self.dtype
+ self.widget = 'checkbox'
end
diff --git a/web/client.js b/web/client.js
index 24d335f..4a9d396 100644
--- a/web/client.js
+++ b/web/client.js
@@ -25,13 +25,15 @@ require(
"acf2/statusbar",
"acf2/transaction",
"acf2/type",
+ "acf2/widget/inline",
"jquery",
"underscore",
+ "acf2/layout/stacked",
+ "acf2/layout/tabular",
"jquery-bbq",
- "jquery-blockui",
- "jquery-ui/sortable"
+ "jquery-blockui"
],
- function(dom, formatError, pth, statusBar, txnMgr, type, $, _) {
+ function(dom, formatError, pth, statusBar, txnMgr, type, Inline, $, _) {
$("#login").submit(function() {
$.ajax("/login", {
@@ -47,787 +49,6 @@ require(
);
- 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) {
- dom.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 = dom.href().click(function() {
- data.delete(name).done(function(txnValid) {
- $("#content").trigger("reload", [txnValid]);
- })
- }).text("Delete");
- this.wrapped = dom.makeRow(this.wrapped);
- if (this.wrapped.is("tr")) link = $("<td>").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)
- dom.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 $("<td>").text(value);
- },
-
- makeEl: function() {
- this.super(Field, "makeEl");
- if (!this.field) this.field = this.el;
- },
-
- createEl: function() { return $("<input>"); },
-
- render: function(value, meta) {
- this.field.attr({type: "text", value: value});
- },
-
- wrap: function() {
- var td = $("<td>");
- this.msg = $("<div>");
- 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 + "<br/>" + 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 $("<select>"); },
-
- staticRender: function(value, meta) {
- return this.super(
- ComboBox,
- "staticRender",
- _.findWhere(meta.choice, {value: value})["ui-value"],
- meta
- );
- },
-
- render: function(value, meta) {
- var el = this.field.empty();
-
- 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(
- meta.choice,
- function(choice) {
- var selected = value == choice.value;
- if (choice.enabled || selected)
- opt(choice.value, choice["ui-value"], selected);
- }
- );
- }
- });
-
- var CheckBox = Field.extend({
- staticRender: function(value, meta) {
- return $("<td>").text(value ? "Yes" : "No");
- },
-
- statusEl: function() { return this.el.parent(); },
-
- render: function(value, meta) {
- this.field.attr({type: "checkbox", checked: value});
- },
-
- get: function() { return this.field.is(":checked"); }
- });
-
-
- var Link = Widget.extend({
- staticRender: function(value, meta) {
- return $("<td>").html(dom.objectRef(value));
- },
-
- createEl: dom.href,
-
- render: function(value, meta) { dom.objectRef(value, this.el) },
-
- wrap: function() { return $("<td>").html(this.el); },
-
- get: function() { return {}; }
- });
-
-
- var Inline = Link.extend({
- init: function(
- data, name, meta, level, editable, removable
- ) {
- return this.super(
- Inline,
- "init",
- data,
- name,
- meta,
- Math.min(6, level + 1),
- editable,
- removable
- );
- },
-
- staticRender: function(value, meta) { return null; },
-
- createEl: function() { return $("<div>"); },
-
- showStatus: false,
-
- requestData: function(value, meta) {
- this.path = value;
- return this.refreshData();
- },
-
- refreshData: function() {
- var def = $.Deferred();
- txnMgr.query(this.path).done(function(data) {
- def.resolve(data, data.meta);
- });
- return def;
- },
-
- showHeading: true,
-
- render: function(data, meta) {
- if (this.showHeading)
- this.el.html(
- $("<h" + this.level + ">").text(meta["ui-name"])
- );
- },
-
- wrap: function() { return this.el; },
-
- validate: function(data) {
- this.super(Inline, "validate", data);
-
- if (this.data.match(this.meta.condition)) {
- var valid = data.validate();
- this.setStatus(data.status());
- statusBar.validationReady(valid);
- }
-
- if (this.fields)
- _.each(this.fields, function(field) {
- field.trigger("updated");
- });
- }
- });
-
-
- var InlineFields = Inline.extend({
-
- render: function(data, meta) {
- this.super(InlineFields, "render", data, meta);
-
- this.appendAboveFields(data, meta);
-
- this.reqData = data;
- var self = this;
-
- if (meta.type == "model") {
- this.fields = {};
- _.each(meta.fields, function(field) {
- if (field.visible)
- self.fields[field.name] = self.renderField(
- field.name,
- field,
- field["ui-name"],
- true,
- false
- );
- });
-
- _.each(this.fields, function(f1, name) {
- _.each(self.fields, function(f2) {
- if (f1 != f2)
- f1.on("validated", function(event) {
- f2.trigger("updated", [name]);
- event.stopPropagation();
- });
- });
- });
-
- _.each(this.fields, function(field) {
- field.trigger("start");
- });
- }
-
- else _.each(data.data, function(value, name) {
- if (meta.type == "set") name = data.data[name];
- else if (_.isArray(data.data)) name++;
- self.renderCollectionMember(name, meta);
- });
-
- this.appendBelowFields(data, meta);
- },
-
- appendAboveFields: function(data, meta) {},
- appendBelowFields: function(data, meta) {},
-
- renderField: function(
- name, meta, label, editable, removable
- ) {
- var widget = this.widget(meta).new(
- this.reqData,
- name,
- meta,
- this.level,
- editable,
- removable
- );
- var container = this.appendWidget(widget, label);
- widget.on("setVisible", function(event, visible) {
- if (visible) container.show();
- else container.hide();
- event.stopPropagation();
- });
- return widget;
- },
-
- renderCollectionMember: function(name, meta) {
- var set = meta.type == "set";
- this.renderField(
- name,
- meta.members,
- meta["ui-member"] + " " + name,
- !set,
- !set && _.contains(meta.removable, name)
- ).trigger("start");
- },
-
- widget: function(meta) { return widgets[meta.widget]; }
- });
-
-
- var Horizontal = InlineFields.extend({
- createEl: function() {
- this.previous = null;
- return $("<tr>").html($("<td>").prop(
- "class", "placeholder"
- ));
- },
-
- showHeading: false,
-
- appendWidget: function(el, label) {
- if (!el.is("td")) return null;
- if (this.previous) this.previous.after(el);
- else {
- var ph = this.el.find(".placeholder");
- ph.after(el);
- ph.remove();
- }
- this.previous = el;
- return el;
- }
- });
-
-
- var HeaderHorizontal = Horizontal.extend({
- init: function(
- data, name, meta, level, editable, removable
- ) {
- this.header = $("<tr>");
- var table = $("<table>");
- table.append($("<thead>").html(this.header));
- table.append($("<tbody>").html(
- this.super(
- HeaderHorizontal,
- "init",
- data,
- name,
- meta,
- level,
- editable,
- removable
- )
- ));
- return table;
- },
-
- appendWidget: function(el, label) {
- el = this.super(
- HeaderHorizontal, "appendWidget", el, label
- );
- if (el) this.header.append($("<th>").text(label));
- return el;
- }
- });
-
-
- var Vertical = InlineFields.extend({
- wrap: function() { return $("<div>").html(this.el); },
-
- appendAboveFields: function(data, meta) {
- this.div = $("<div>");
- this.el.append(this.div);
-
- var self = this;
-
- if (meta.type == "model")
- _.each(meta.actions, function(action) {
- self.div.append($("<input>").attr(
- {type: "submit", value: action["ui-name"]}
- ).click(function() {
- data.invoke(action.name).done(function() {
- alert("Done");
- }).fail(function() {
- alert("Failed");
- });
- }));
- });
- },
-
- appendBelowFields: function(data, meta) {
- if (meta.editable &&
- _.contains(["collection", "list"], meta.type)) {
- if (!this.table) this.makeSortable(this.div);
-
- var self = this;
- var keys = _.clone(_.keys(data.data));
-
- var button = $("<input>").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,
- type.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 = $("<input>").attr({type: "text"});
- var row = $("<tr>").html($("<td>").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($("<p>").html(button));
- }
- },
-
- appendWidget: function(el, label) {
- var self = this;
- el = dom.makeRow(el);
-
- if (el.is("tr")) {
- el.prepend($("<td>").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 = $("<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($("<tbody>"));
- this.div.append($("<table>").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) {
- $("#content").trigger(
- "reload", [txnValid]
- );
- });
- }
- });
- return el;
- }
- });
-
-
- var CheckBoxes = Inline.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 = $("<tbody>");
- this.el.append($("<table>").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 = $("<input>").attr({
- type: "checkbox", checked: selected
- });
-
- var row = $("<tr>");
- row.append($("<td>").html(cbox));
-
- var item = $("<td>");
- if (choice.ref)
- item.html(dom.objectRef(choice.ref)
- .text(choice["ui-value"]));
- else item.text(choice["ui-value"]);
- row.append(item);
-
- function setRowStatus() {
- dom.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 $("<td>").html(dom.objectRef(value).text(value));
- },
-
- statusEl: function() { return this.el.find("select"); },
-
- createEl: function() { return $("<div>"); },
-
- render: function(value, meta) {
- this.super(Reference, "render", value, meta);
-
- this.el.html(this.field);
- this.el.append(" ");
-
- var link = $("<div>");
- var update = _.bind(function() {
- link.html(dom.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 (!type.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 type.isTreeNode(meta) ?
- Vertical : this.super(Stacked, "widget", meta);
- }
- });
-
-
- var layouts = {stacked: Stacked, tabular: Tabular};
-
-
function redirect(path) { $.bbq.pushState("#" + path); }
function renderObject(path, data) {
@@ -838,7 +59,9 @@ require(
).done(function(data) {
var layout = data.meta.widget;
var name = pth.split(path).pop();
- (layout ? layouts[layout] : Vertical).extend({
+ (layout ?
+ require("acf2/layout/" + layout) :
+ Inline).extend({
createEl: function() { return $("#content").empty(); },
wrap: function() { return this.el; },
requestData: function(value, meta) {
diff --git a/web/layout/stacked.js b/web/layout/stacked.js
new file mode 100644
index 0000000..1fa8af9
--- /dev/null
+++ b/web/layout/stacked.js
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2012-2014 Kaarle Ritvanen
+ * See LICENSE file for license details
+ */
+
+define(["acf2/type", "acf2/widget/inline"], function(type, Base) {
+ var Class = Base.extend({
+ widget: function(meta) {
+ return type.isTreeNode(meta) ?
+ Base : this.super(Class, "widget", meta);
+ }
+ });
+
+ return Class;
+});
+
+
diff --git a/web/layout/tabular.js b/web/layout/tabular.js
new file mode 100644
index 0000000..03fbe03
--- /dev/null
+++ b/web/layout/tabular.js
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2012-2014 Kaarle Ritvanen
+ * See LICENSE file for license details
+ */
+
+define(
+ [
+ "acf2/type",
+ "acf2/widget/inline",
+ "acf2/widget/table/header",
+ "acf2/widget/table/row"
+ ],
+ function(type, Base, TableHeader, TableRow) {
+ var Class = Base.extend({
+ render: function(data, meta) {
+ this.header = true;
+ this.super(Class, "render", data, meta);
+ },
+
+ widget: function(meta) {
+ if (!type.isTreeNode(meta))
+ return this.super(Class, "widget", meta);
+ if (!this.header) return TableRow;
+ this.header = false;
+ return TableHeader;
+ }
+ });
+
+ return Class;
+ }
+);
diff --git a/web/widget/abstract/base.js b/web/widget/abstract/base.js
new file mode 100644
index 0000000..4a49e0b
--- /dev/null
+++ b/web/widget/abstract/base.js
@@ -0,0 +1,134 @@
+/*
+ * Copyright (c) 2012-2014 Kaarle Ritvanen
+ * See LICENSE file for license details
+ */
+
+define(["acf2/dom", "jquery", "underscore"], function(dom, $, _) {
+ return {
+ 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) {
+ dom.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 = dom.href().click(function() {
+ data.delete(name).done(function(txnValid) {
+ $("#content").trigger("reload", [txnValid]);
+ })
+ }).text("Delete");
+ this.wrapped = dom.makeRow(this.wrapped);
+ if (this.wrapped.is("tr")) link = $("<td>").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)
+ dom.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(); }
+ };
+});
diff --git a/web/widget/abstract/fields.js b/web/widget/abstract/fields.js
new file mode 100644
index 0000000..73111ce
--- /dev/null
+++ b/web/widget/abstract/fields.js
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2012-2014 Kaarle Ritvanen
+ * See LICENSE file for license details
+ */
+
+define(
+ [
+ "acf2/widget/abstract/inline",
+ "jquery",
+ "underscore",
+ "acf2/widget/checkbox",
+ "acf2/widget/checkboxes",
+ "acf2/widget/combobox",
+ "acf2/widget/field",
+ "acf2/widget/inline",
+ "acf2/widget/reference"
+ ],
+ function(Base, $, _) {
+ var Class = Base.extend({
+ render: function(data, meta) {
+ this.super(Class, "render", data, meta);
+
+ this.appendAboveFields(data, meta);
+
+ this.reqData = data;
+ var self = this;
+
+ if (meta.type == "model") {
+ this.fields = {};
+ _.each(meta.fields, function(field) {
+ if (field.visible)
+ self.fields[field.name] = self.renderField(
+ field.name,
+ field,
+ field["ui-name"],
+ true,
+ false
+ );
+ });
+
+ _.each(this.fields, function(f1, name) {
+ _.each(self.fields, function(f2) {
+ if (f1 != f2)
+ f1.on("validated", function(event) {
+ f2.trigger("updated", [name]);
+ event.stopPropagation();
+ });
+ });
+ });
+
+ _.each(this.fields, function(field) {
+ field.trigger("start");
+ });
+ }
+
+ else _.each(data.data, function(value, name) {
+ if (meta.type == "set") name = data.data[name];
+ else if (_.isArray(data.data)) name++;
+ self.renderCollectionMember(name, meta);
+ });
+
+ this.appendBelowFields(data, meta);
+ },
+
+ appendAboveFields: function(data, meta) {},
+ appendBelowFields: function(data, meta) {},
+
+ renderField: function(
+ name, meta, label, editable, removable
+ ) {
+ var widget = this.widget(meta).new(
+ this.reqData,
+ name,
+ meta,
+ this.level,
+ editable,
+ removable
+ );
+ var container = this.appendWidget(widget, label);
+ widget.on("setVisible", function(event, visible) {
+ if (visible) container.show();
+ else container.hide();
+ event.stopPropagation();
+ });
+ return widget;
+ },
+
+ renderCollectionMember: function(name, meta) {
+ var set = meta.type == "set";
+ this.renderField(
+ name,
+ meta.members,
+ meta["ui-member"] + " " + name,
+ !set,
+ !set && _.contains(meta.removable, name)
+ ).trigger("start");
+ },
+
+ widget: function(meta) {
+ return require("acf2/widget/" + meta.widget);
+ }
+ });
+
+ return Class;
+ }
+);
diff --git a/web/widget/abstract/inline.js b/web/widget/abstract/inline.js
new file mode 100644
index 0000000..af7d110
--- /dev/null
+++ b/web/widget/abstract/inline.js
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2012-2014 Kaarle Ritvanen
+ * See LICENSE file for license details
+ */
+
+define(
+ ["acf2/statusbar", "acf2/widget/link", "jquery", "underscore"],
+ function(statusBar, Base, $, _) {
+ var Class = Base.extend({
+ init: function(
+ data, name, meta, level, editable, removable
+ ) {
+ this.txnMgr = data.txnMgr;
+ return this.super(
+ Class,
+ "init",
+ data,
+ name,
+ meta,
+ Math.min(6, level + 1),
+ editable,
+ removable
+ );
+ },
+
+ staticRender: function(value, meta) { return null; },
+
+ createEl: function() { return $("<div>"); },
+
+ showStatus: false,
+
+ requestData: function(value, meta) {
+ this.path = value;
+ return this.refreshData();
+ },
+
+ refreshData: function() {
+ var def = $.Deferred();
+ this.txnMgr.query(this.path).done(function(data) {
+ def.resolve(data, data.meta);
+ });
+ return def;
+ },
+
+ showHeading: true,
+
+ render: function(data, meta) {
+ if (this.showHeading)
+ this.el.html(
+ $("<h" + this.level + ">").text(meta["ui-name"])
+ );
+ },
+
+ wrap: function() { return this.el; },
+
+ validate: function(data) {
+ this.super(Class, "validate", data);
+
+ if (this.data.match(this.meta.condition)) {
+ var valid = data.validate();
+ this.setStatus(data.status());
+ statusBar.validationReady(valid);
+ }
+
+ if (this.fields)
+ _.each(this.fields, function(field) {
+ field.trigger("updated");
+ });
+ }
+ });
+
+ return Class;
+ }
+);
diff --git a/web/widget/checkbox.js b/web/widget/checkbox.js
new file mode 100644
index 0000000..543433c
--- /dev/null
+++ b/web/widget/checkbox.js
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2012-2014 Kaarle Ritvanen
+ * See LICENSE file for license details
+ */
+
+define(["acf2/widget/field", "jquery"], function(Base, $) {
+ return Base.extend({
+ staticRender: function(value, meta) {
+ return $("<td>").text(value ? "Yes" : "No");
+ },
+
+ statusEl: function() { return this.el.parent(); },
+
+ render: function(value, meta) {
+ this.field.attr({type: "checkbox", checked: value});
+ },
+
+ get: function() { return this.field.is(":checked"); }
+ });
+});
diff --git a/web/widget/checkboxes.js b/web/widget/checkboxes.js
new file mode 100644
index 0000000..a34aced
--- /dev/null
+++ b/web/widget/checkboxes.js
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2012-2014 Kaarle Ritvanen
+ * See LICENSE file for license details
+ */
+
+define(
+ [
+ "acf2/dom",
+ "acf2/statusbar",
+ "acf2/widget/abstract/inline",
+ "jquery",
+ "underscore"
+ ],
+ function(dom, statusBar, Base, $, _) {
+ var Class = Base.extend({
+ showStatus: true,
+
+ setStatus: function(status) {
+ this.super(
+ Class,
+ "setStatus",
+ status == "invalid" ? "invalid" : null
+ );
+ },
+
+ render: function(data, meta) {
+ this.dynamic = meta.members.dynamic;
+
+ this.super(Class, "render", data, meta);
+
+ var table = $("<tbody>");
+ this.el.append($("<table>").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 = $("<input>").attr({
+ type: "checkbox", checked: selected
+ });
+
+ var row = $("<tr>");
+ row.append($("<td>").html(cbox));
+
+ var item = $("<td>");
+ if (choice.ref)
+ item.html(dom.objectRef(choice.ref)
+ .text(choice["ui-value"]));
+ else item.text(choice["ui-value"]);
+ row.append(item);
+
+ function setRowStatus() {
+ dom.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);
+ });
+ }
+ });
+
+ return Class;
+ }
+);
diff --git a/web/widget/combobox.js b/web/widget/combobox.js
new file mode 100644
index 0000000..72761ee
--- /dev/null
+++ b/web/widget/combobox.js
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2012-2014 Kaarle Ritvanen
+ * See LICENSE file for license details
+ */
+
+define(["acf2/widget/field", "jquery", "underscore"], function(Base, $, _) {
+ var Class = Base.extend({
+ createEl: function() { return $("<select>"); },
+
+ staticRender: function(value, meta) {
+ return this.super(
+ Class,
+ "staticRender",
+ _.findWhere(meta.choice, {value: value})["ui-value"],
+ meta
+ );
+ },
+
+ render: function(value, meta) {
+ var el = this.field.empty();
+
+ 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(
+ meta.choice,
+ function(choice) {
+ var selected = value == choice.value;
+ if (choice.enabled || selected)
+ opt(choice.value, choice["ui-value"], selected);
+ }
+ );
+ }
+ });
+
+ return Class;
+});
diff --git a/web/widget/field.js b/web/widget/field.js
new file mode 100644
index 0000000..e4524e1
--- /dev/null
+++ b/web/widget/field.js
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2012-2014 Kaarle Ritvanen
+ * See LICENSE file for license details
+ */
+
+define(
+ [
+ "acf2/error",
+ "acf2/statusbar",
+ "acf2/widget/abstract/base",
+ "jquery",
+ "underscore"
+ ],
+ function(formatError, statusBar, Base, $, _) {
+ var Class = Base.extend({
+ init: function(
+ data, name, meta, level, editable, removable
+ ) {
+ this.editable = editable && meta.editable;
+
+ var el = this.super(
+ Class,
+ "init",
+ data,
+ name,
+ meta,
+ level,
+ editable,
+ removable
+ );
+
+ if (this.editable)
+ this.field.change(_.bind(this.validateChange, this));
+
+ return el;
+ },
+
+ staticRender: function(value, meta) {
+ return $("<td>").text(value);
+ },
+
+ makeEl: function() {
+ this.super(Class, "makeEl");
+ if (!this.field) this.field = this.el;
+ },
+
+ createEl: function() { return $("<input>"); },
+
+ render: function(value, meta) {
+ this.field.attr({type: "text", value: value});
+ },
+
+ wrap: function() {
+ var td = $("<td>");
+ this.msg = $("<div>");
+ td.append(this.msg);
+ td.append(this.el);
+ return td;
+ },
+
+ validate: function(value) {
+ this.super(Class, "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 + "<br/>" + 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);
+ }
+ });
+
+ return Class;
+ }
+);
diff --git a/web/widget/inline.js b/web/widget/inline.js
new file mode 100644
index 0000000..b354f5c
--- /dev/null
+++ b/web/widget/inline.js
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2012-2014 Kaarle Ritvanen
+ * See LICENSE file for license details
+ */
+
+define(
+ [
+ "acf2/dom",
+ "acf2/statusbar",
+ "acf2/type",
+ "acf2/widget/abstract/fields",
+ "jquery",
+ "underscore",
+ "jquery-ui/sortable"
+ ],
+ function(dom, statusBar, type, Base, $, _) {
+ return Base.extend({
+ wrap: function() { return $("<div>").html(this.el); },
+
+ appendAboveFields: function(data, meta) {
+ this.div = $("<div>");
+ this.el.append(this.div);
+
+ var self = this;
+
+ if (meta.type == "model")
+ _.each(meta.actions, function(action) {
+ self.div.append($("<input>").attr(
+ {type: "submit", value: action["ui-name"]}
+ ).click(function() {
+ data.invoke(action.name)
+ .done(function() { alert("Done"); })
+ .fail(function() { alert("Failed"); });
+ }));
+ });
+ },
+
+ appendBelowFields: function(data, meta) {
+ if (meta.editable &&
+ _.contains(["collection", "list"], meta.type)) {
+ if (!this.table) this.makeSortable(this.div);
+
+ var self = this;
+ var keys = _.clone(_.keys(data.data));
+
+ var button = $("<input>").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, type.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 = $("<input>").attr({type: "text"});
+ var row = $("<tr>").html($("<td>").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($("<p>").html(button));
+ }
+ },
+
+ appendWidget: function(el, label) {
+ var self = this;
+ el = dom.makeRow(el);
+
+ if (el.is("tr")) {
+ el.prepend($("<td>").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 = $("<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($("<tbody>"));
+ this.div.append($("<table>").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) {
+ $("#content").trigger("reload", [txnValid]);
+ });
+ }
+ });
+ return el;
+ }
+ });
+ }
+);
diff --git a/web/widget/link.js b/web/widget/link.js
new file mode 100644
index 0000000..f6539c6
--- /dev/null
+++ b/web/widget/link.js
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2012-2014 Kaarle Ritvanen
+ * See LICENSE file for license details
+ */
+
+define(
+ ["acf2/dom", "acf2/widget/abstract/base", "jquery"],
+ function(dom, Base, $) {
+ return Base.extend({
+ staticRender: function(value, meta) {
+ return $("<td>").html(dom.objectRef(value));
+ },
+
+ createEl: dom.href,
+
+ render: function(value, meta) { dom.objectRef(value, this.el) },
+
+ wrap: function() { return $("<td>").html(this.el); },
+
+ get: function() { return {}; }
+ });
+ }
+);
diff --git a/web/widget/reference.js b/web/widget/reference.js
new file mode 100644
index 0000000..4413cc9
--- /dev/null
+++ b/web/widget/reference.js
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2012-2014 Kaarle Ritvanen
+ * See LICENSE file for license details
+ */
+
+define(
+ ["acf2/dom", "acf2/widget/combobox", "jquery", "underscore"],
+ function(dom, Base, $, _) {
+ var Class = Base.extend({
+ init: function(
+ data, name, meta, level, editable, removable
+ ) {
+ this.field = Base.createEl();
+ return this.super(
+ Class,
+ "init",
+ data,
+ name,
+ meta,
+ level,
+ editable,
+ removable
+ );
+ },
+
+ staticRender: function(value, meta) {
+ return $("<td>").html(dom.objectRef(value).text(value));
+ },
+
+ statusEl: function() { return this.el.find("select"); },
+
+ createEl: function() { return $("<div>"); },
+
+ render: function(value, meta) {
+ this.super(Class, "render", value, meta);
+
+ this.el.html(this.field);
+ this.el.append(" ");
+
+ var link = $("<div>");
+ var update = _.bind(function() {
+ link.html(dom.objectRef(this.get()));
+ }, this);
+ this.el.append(link);
+
+ this.field.change(update);
+ update();
+ }
+ });
+
+ return Class;
+ }
+);
diff --git a/web/widget/table/header.js b/web/widget/table/header.js
new file mode 100644
index 0000000..0dce26e
--- /dev/null
+++ b/web/widget/table/header.js
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2012-2014 Kaarle Ritvanen
+ * See LICENSE file for license details
+ */
+
+define(["acf2/widget/table/row", "jquery"], function(Base, $) {
+ var Class = Base.extend({
+ init: function(
+ data, name, meta, level, editable, removable
+ ) {
+ this.header = $("<tr>");
+ var table = $("<table>");
+ table.append($("<thead>").html(this.header));
+ table.append($("<tbody>").html(
+ this.super(
+ Class,
+ "init",
+ data,
+ name,
+ meta,
+ level,
+ editable,
+ removable
+ )
+ ));
+ return table;
+ },
+
+ appendWidget: function(el, label) {
+ el = this.super(Class, "appendWidget", el, label);
+ if (el) this.header.append($("<th>").text(label));
+ return el;
+ }
+ });
+
+ return Class;
+});
diff --git a/web/widget/table/row.js b/web/widget/table/row.js
new file mode 100644
index 0000000..23aadc3
--- /dev/null
+++ b/web/widget/table/row.js
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2012-2014 Kaarle Ritvanen
+ * See LICENSE file for license details
+ */
+
+define(["acf2/widget/abstract/fields", "jquery"], function(Base, $) {
+ return Base.extend({
+ createEl: function() {
+ this.previous = null;
+ return $("<tr>").html($("<td>").prop("class", "placeholder"));
+ },
+
+ showHeading: false,
+
+ appendWidget: function(el, label) {
+ if (!el.is("td")) return null;
+ if (this.previous) this.previous.after(el);
+ else {
+ var ph = this.el.find(".placeholder");
+ ph.after(el);
+ ph.remove();
+ }
+ this.previous = el;
+ return el;
+ }
+ });
+});