diff options
author | Kaarle Ritvanen <kaarle.ritvanen@datakunkku.fi> | 2014-02-06 23:09:13 +0200 |
---|---|---|
committer | Kaarle Ritvanen <kaarle.ritvanen@datakunkku.fi> | 2014-02-14 22:35:56 +0200 |
commit | 38ac196b2f00c1e13a0928cd892384bc296c8a98 (patch) | |
tree | 473663650b5b4fd898bd9bab4c666dca6b900dd4 | |
parent | 7d434464a843a8ca8acc903367492186a0ae13e8 (diff) | |
download | acf2-38ac196b2f00c1e13a0928cd892384bc296c8a98.tar.bz2 acf2-38ac196b2f00c1e13a0928cd892384bc296c8a98.tar.xz |
web client: extract each widget to a dedicated file
-rw-r--r-- | acf2/model/field.lua | 2 | ||||
-rw-r--r-- | web/client.js | 793 | ||||
-rw-r--r-- | web/layout/stacked.js | 17 | ||||
-rw-r--r-- | web/layout/tabular.js | 31 | ||||
-rw-r--r-- | web/widget/abstract/base.js | 134 | ||||
-rw-r--r-- | web/widget/abstract/fields.js | 106 | ||||
-rw-r--r-- | web/widget/abstract/inline.js | 74 | ||||
-rw-r--r-- | web/widget/checkbox.js | 20 | ||||
-rw-r--r-- | web/widget/checkboxes.js | 78 | ||||
-rw-r--r-- | web/widget/combobox.js | 42 | ||||
-rw-r--r-- | web/widget/field.js | 109 | ||||
-rw-r--r-- | web/widget/inline.js | 155 | ||||
-rw-r--r-- | web/widget/link.js | 23 | ||||
-rw-r--r-- | web/widget/reference.js | 53 | ||||
-rw-r--r-- | web/widget/table/header.js | 37 | ||||
-rw-r--r-- | web/widget/table/row.js | 27 |
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; + } + }); +}); |