(function($) {
/**
* twentyc.edit module that provides inline editing tools and functionality
* for web content
*
* @module twentyc
* @class editable
* @static
*/
/// Contains several user facing validation sentences, translated
twentyc.editable = {
/**
* initialze all edit-enabled content
*
* called automatically on page load
*
* @method init
* @private
*/
init : function() {
if(this.initialized)
return;
this.templates.init();
$('[data-edit-target]').editable();
// hook into data load so we can update selects with matching datasets
$(twentyc.data).on("load", function(ev, payload) {
$('select[data-edit-data="'+payload.id+'"]').each(function(idx) {
$(this).data("edit-input").load(payload.data)
});
});
// init modules
$('[data-edit-module]').each(function(idx) {
var module = twentyc.editable.module.instantiate($(this));
module.init();
});
// initialize always toggled inputs
$('.editable.always').not(".auto-toggled").each(function(idx) {
var container = $(this);
container.find('[data-edit-type]').editable(
'filter', { belongs : container }
).each(function(idx) {
$(this).data("edit-always", true);
twentyc.editable.input.manage($(this), container);
});
});
this.initialized = true;
}
}
/**
* humanize editable errors
*
* @module twentyc
* @namespace editable
* @class error
* @static
*/
twentyc.editable.error = {
/**
* humanize the error of the specified type
*
* @method humanize
* @param {String} errorType error type string (e.g. "ValidationErrors")
* @returns {String} humanizedString
*/
humanize : function(errorType) {
switch(errorType) {
case "ValidationErrors":
return gettext("Some of the fields contain invalid values - please correct and try again."); ///
break;
default:
return gettext("Something went wrong."); ///
break;
}
}
}
/**
* container for action handler
*
* @module twentyc
* @namespace editable
* @class action
* @extends twentyc.cls.Registry
* @static
*/
twentyc.editable.action = new twentyc.cls.Registry();
twentyc.editable.action.register(
"base",
{
name : function() {
return this._meta.name;
},
execute : function(trigger, container) {
this.trigger = trigger
this.container = container;
if(this.loading_shim)
this.container.children('.editable.loading-shim').show();
},
signal_error : function(container, error) {
var payload = {
reason : error.type,
info : error.info,
data : error.data
}
container.trigger("action-error", payload);
container.trigger("action-error:"+this.name(), payload);
$(this).trigger("error", payload);
if(this.loading_shim)
this.container.children('.editable.loading-shim').hide();
},
signal_success : function(container, payload) {
container.trigger("action-success", payload);
container.trigger("action-success:"+this.name(), payload);
$(this).trigger("success", payload);
if(this.loading_shim)
this.container.children('.editable.loading-shim').hide();
}
}
);
twentyc.editable.action.register(
"toggle-edit",
{
execute : function(trigger, container) {
this.base_execute(trigger, container);
container.editable("toggle");
container.trigger("action-success:toggle", { mode : container.data("edit-mode") });
}
},
"base"
);
twentyc.editable.action.register(
"reset",
{
execute : function(trigger, container) {
container.editable("reset");
this.signal_success(container, {});
}
},
"base"
);
twentyc.editable.action.register(
"submit",
{
loading_shim : true,
execute : function(trigger, container) {
this.base_execute(trigger, container);
var me = this,
modules = [],
targets = 1,
changed,
status={"error":false, "data":{}},
i;
var dec_targets = function(ev,data,error) {
targets--;
if(error)
status.error = true;
if(data) {
$.extend(status.data, data);
}
if(!targets) {
if(!status.error && !me.noToggle) {
container.trigger("action-success:toggle", {mode:"view"})
container.editable("toggle", { data:status.data });
}
/*
if(!status.error && container.data("edit-always")) {
// if container is always toggled to edit mode
// update the original_value property of the
// input instance, so we can properly pick up
// changes for future edits
container.editable("accept-values");
}
*/
container.editable("loading-shim", "hide");
}
}
try {
// try creating target - this automatically parses form data
// into object literal
var target = twentyc.editable.target.instantiate(container);
changed = target.data._changed;
$.extend(status.data, target.data);
// prepare modules
container.find("[data-edit-module]").
//editable("filter", { belongs : container }).
each(function(idx) {
var module = twentyc.editable.module.instantiate($(this));
if(!module.has_action("submit")) {
module.prepare();
if(module.pending_submit.length) {
targets+=module.pending_submit.length;
modules.push([module, $(this)])
}
}
});
} catch(error) {
// we need to catch editable errors (identified by having type
// set and fire off an event in case of failure - this also
// catches validation errors
if(error.type) {
return this.signal_error(container, error);
} else {
// unknown errors are re-thrown so the browser can catch
// them properly
throw(error);
}
}
var grouped = container.editable("filter", { grouped : true }).not("[data-edit-module]");
grouped.each(function(idx) {
var target = twentyc.editable.target.instantiate($(this));
$.extend(status.data, target.data);
if(target.data._changed) {
targets += 1
}
});
if(changed || container.data("edit-always-submit") == "yes"){
$(target).on("success", function(ev, data) {
me.signal_success(container, data);
});
$(target).on("error", function(ev, error) {
me.signal_error(container, error);
dec_targets({}, {}, true);
});
$(target).on("success", dec_targets);
// submit main target
var result = target.execute();
} else {
dec_targets({}, {});
}
// submit grouped targets
grouped.each(function(idx) {
var other = $(this);
var action = new (twentyc.editable.action.get("submit"))();
action.noToggle = true;
$(action).on("success",dec_targets);
$(action).on("error", function(){dec_targets({},{},true);});
action.execute(trigger, other);
});
// submit modules
for(i in modules) {
$(modules[i][0]).on("success", dec_targets);
$(modules[i][0]).on("error", function(){dec_targets({},{},true);});
modules[i][0].execute(trigger, modules[i][1]);
}
return result;
}
},
"base"
);
twentyc.editable.action.register(
"module-action",
{
name : function() {
return this.module._meta.name+"."+this.actionName;
},
execute : function(module, action, trigger, container) {
this.base_execute(trigger, container);
this.module = module;
this.actionName = action;
module.action = this;
$(module.target).on("success", function(ev, d) {
module.action.signal_success(container, d);
$(module).trigger("success", [d]);
});
$(module.target).on("error", function(ev, error) {
module.action.signal_error(container, error);
$(module).trigger("error", [error]);
});
try {
this.module["execute_"+action](trigger, container);
} catch(error) {
if(error.type) {
return this.signal_error(container, error);
} else {
// unknown errors are re-thrown so the browser can catch
// them properly
throw(error);
}
}
}
},
"base"
);
/**
* container for module handler
*
* @module twentyc
* @namespace editable
* @class module
* @extends twentyc.cls.Registry
* @static
*/
twentyc.editable.module = new twentyc.cls.Registry();
twentyc.editable.module.instantiate = function(container) {
var module = new (this.get(container.data("edit-module")))(container);
return module;
};
/**
* base module to use for all editable modules
*
* modules allow you add custom behaviour to forms / editing process
*
* @class base
* @namespace twentuc.editable.module
* @constructor
*/
twentyc.editable.module.register(
"base",
{
init : function() {
return;
},
has_action : function(action) {
return this.container.find('[data-edit-action="'+action+'"]').length > 0;
},
base : function(container) {
var comp = this.components = {};
container.find("[data-edit-component]").editable("filter",{belongs:container}).each(function(idx) {
var c = $(this);
comp[c.data("edit-component")] = c;
});
this.container = container;
container.data("edit-module-instance", this);
},
get_target : function(container) {
return twentyc.editable.target.instantiate(container || this.container);
},
execute : function(trigger, container) {
var me = $(this), action = trigger.data("edit-action");
this.trigger = trigger;
this.target = twentyc.editable.target.instantiate(container);
handler = new (twentyc.editable.action.get("module-action"))
handler.loading_shim = this.loading_shim;
handler.execute(this, action, trigger, container);
},
prepare : function() { this.prepared = true },
execute_submit : function(trigger, container) {
return;
}
}
);
/**
* this module allows you maintain a listing of items with functionality
* to add, remove and change the items.
*
* @class listing
* @namespace twentyc.editable.module
* @constructor
* @extends twentyc.editable.module.base
*/
twentyc.editable.module.register(
"listing",
{
pending_submit : [],
init : function() {
// a template has been specified for the add form
// try to build add row form from it
if(this.components.add && this.components.add.data("edit-template")) {
var addrow = twentyc.editable.templates.copy(this.components.add.data("edit-template"));
this.components.add.prepend(addrow);
}
if(this.container.data("edit-always")) {
var me = this;
this.container.on("listing:row-submit", function() {
me.components.list.editable("accept-values");
});
}
},
prepare : function() {
if(this.prepared)
return;
var pending = this.pending_submit = [];
var me = this;
this.components.list.children().each(function(idx) {
var row = $(this),
data = {};
var changedFields = row.find("[data-edit-type]").
editable("filter", "changed").
editable("filter", { belongs : me.components.list }, true);
if(changedFields.length == 0)
return;
row.find("[data-edit-type]").editable("filter", { belongs : me.components.list }).editable("export-fields", data);
row.editable("collect-payload", data);
pending.push({ row : row, data : data, id : row.data("edit-id")});
});
this.base_prepare();
},
row : function(trigger) {
return trigger.closest("[data-edit-id]").first();
},
row_id : function(trigger) {
return this.row(trigger).data("edit-id")
},
clear : function() {
this.components.list.empty();
},
add : function(rowId, trigger, container, data) {
var row = twentyc.editable.templates.copy(this.components.list.data("edit-template"))
var k;
row.attr("data-edit-id", rowId);
row.data("edit-id", rowId);
for(k in data) {
row.find('[data-edit-name="'+k+'"]').each(function(idx) {
$(this).text(data[k]);
$(this).data("edit-value", data[k]);
});
}
row.appendTo(this.components.list);
row.addClass("newrow");
container.editable("sync");
if(this.action)
this.action.signal_success(container, rowId);
container.trigger("listing:row-add", [rowId, row, data, this]);
this.components.list.scrollTop(function() { return this.scrollHeight; });
return row;
},
remove : function(rowId, row, trigger, container) {
row.detach();
if(this.action)
this.action.signal_success(container, rowId);
container.trigger("listing:row-remove", [rowId, row, this]);
},
submit : function(rowId, data, row, trigger, container) {
if(this.action)
this.action.signal_success(container, rowId);
container.trigger("listing:row-submit", [rowId, row, data, this]);
},
execute_submit : function(trigger, container) {
var i, P;
this.prepare();
if(!this.pending_submit.length) {
if(this.action)
this.action.signal_success(container);
return;
}
for(i in this.pending_submit) {
P = this.pending_submit[i];
this.submit(P.id, P.data, P.row, trigger, container);
}
},
execute_add : function(trigger, container) {
var data = {};
this.components.add.editable("export", data);
this.data = data
this.add(null,trigger, container, data);
},
execute_remove : function(trigger, container) {
var row = trigger.closest("[data-edit-id]").first();
this.remove(row.data("edit-id"), row, trigger, container);
}
},
"base"
);
/**
* allows you to setup and manage target handlers
*
* @module twentyc
* @namespace editable
* @class target
* @static
*/
twentyc.editable.target = new twentyc.cls.Registry();
twentyc.editable.target.error_handlers = {};
twentyc.editable.target.instantiate = function(container) {
var handler,
targetParam = container.data("edit-target").split(":")
// check if specified target has a handler, if not use standard XHR hander
if(!twentyc.editable.target.has(targetParam[0]))
handler = twentyc.editable.target.get("XHRPost")
else
handler = twentyc.editable.target.get(targetParam[0])
// try creating target - this automatically parses form data
// into object literal
return new handler(targetParam, container);
}
twentyc.editable.target.register(
"base",
{
base : function(target, sender) {
this.args = target;
this.label = this.args[0];
this.sender = sender;
this.data = {}
sender.editable("export", this.data)
},
data_clean : function(removeEmpty) {
var i, r = {};
for(i in this.data) {
if(removeEmpty && (this.data[i] === null || this.data[i] === "" || this.data[i] === undefined))
continue;
if(i.charAt(0) != "_")
r[i] = this.data[i];
}
return r;
},
data_valid : function() {
return (this.data && this.data["_valid"]);
},
execute : function() {}
}
);
twentyc.editable.target.register(
"XHRPost",
{
execute : function(appendUrl, context, onSuccess, onFailure) {
var me = $(this), data = this.data;
if(context)
this.context = context;
if(this.context)
var sender = this.context;
else
var sender = this.sender;
$.ajax({
url : this.args[0]+(appendUrl?"/"+appendUrl:""),
method : "POST",
data : this.data_clean(this.data),
success : function(response) {
data.xhr_response = response;
me.trigger("success", data);
if(onSuccess)
onSuccess(response, data)
}
}).fail(function(response) {
twentyc.editable.target.error_handlers.http_json(response, me, sender);
if(onFailure)
onFailure(response)
});
}
},
"base"
)
twentyc.editable.target.error_handlers.http_json = function(response, me, sender) {
var info = [response.status + " " + response.statusText]
if(response.status == 400) {
var msg, k, i, info= [gettext("The server rejected your data")]; ///
for(k in response.responseJSON) {
sender.find('[data-edit-name="'+k+'"], [data-edit-error-field="'+k+'"]').each(function(idx) {
var input = $(this).data("edit-input-instance");
if(input) {
msg = response.responseJSON[k];
if(typeof msg == "object" && msg.join)
msg = msg.join(",");
input.show_validation_error(msg);
}
});
if(k == "non_field_errors") {
for(i in response.responseJSON[k])
info.push(response.responseJSON[k][i]);
}
}
} else {
if(response.responseJSON && response.responseJSON.non_field_errors) {
info = [];
var i;
for(i in response.responseJSON.non_field_errors)
info.push(response.responseJSON.non_field_errors[i]);
}
}
me.trigger(
"error",
{
type : "HTTPError",
info : info.join("
")
}
);
}
/**
* allows you to setup and manage input types
*
* @module twentyc
* @namespace editble
* @class input
* @static
*/
twentyc.editable.input = new (twentyc.cls.extend(
"InputRegistry",
{
frame : function() {
var frame = $('