1
0
mirror of https://github.com/peeringdb/peeringdb.git synced 2024-05-11 05:55:09 +00:00
Files
peeringdb-peeringdb/peeringdb_server/static/20c/twentyc.edit.js
Matt Griswold 7c3d160dec Support 202106 (#994)
* fixes #965: intermittent bug during consolidation of notifications

* fixes #863: better visibility for input validation errors

* fixes #375: re-evaluate affiliation requests on email change

* fixes #741: remove data quality validation for superusers

* fixes #587: customizable pagination in django admin

* fixes #923: Prevent deletion of a last technical contact if there is an existing netixlan object

* better search #23 (first pass)

* black format

* haystack test config to run_tests.py
remove old django_init.py test settings

* black format

* haystack test config fixes

* #23 better search (pt.2)

* rate limit distance queries (#23)
rate limiting based on query filters

* settings controlled blocking of distance filter for unauthenticated / unverified users (#23)

* fix distance filter throttling with api key auth (#23)

* fix anon user access check on distance queries

* search index and performance tweaks

* fix org_id not set in search results

* org id to int

Co-authored-by: Stefan Pratter <stefan@20c.com>
2021-07-07 17:57:04 -05:00

1700 lines
42 KiB
JavaScript

(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("<br />")
}
);
}
/**
* 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 = $('<div class="editable input-frame"></div>');
return frame;
},
wire : function(it, element, container) {
var action = container.data("edit-enter-action");
if(it.action_on_enter && action) {
element.on("keydown", function(e) {
if(e.which == 13) {
handler = new (twentyc.editable.action.get(action));
handler.execute(element, container);
}
});
}
it.element.focus(function(ev) {
it.reset();
});
if(it.wire)
it.wire();
},
manage : function(element, container) {
var it = new (this.get(element.data("edit-type")));
var par = element.parent()
it.container = container;
it.source = element;
it.element = element;
it.frame = this.frame();
it.frame.insertBefore(element);
it.frame.append(element);
it.original_value = it.get();
this.wire(it, it.element, container);
element.data("edit-input-instance", it);
return it;
},
create : function(name, source, container) {
var it = new (this.get(name));
it.source = source
it.container = container
it.element = it.make();
it.frame = this.frame();
it.frame.append(it.element);
it.set(source.data("edit-value"));
it.original_value = it.get();
it.static_elements = source.find('[data-edit-static]')
if(source.data().hasOwnProperty("editResetValue")) {
it.reset_value = source.data("edit-reset-value") || null;
} else {
it.reset_value = it.original_value;
}
if(it.placeholder)
it.element.attr("placeholder", it.placeholder)
else if(it.source.data("edit-placeholder"))
it.element.attr("placeholder", it.source.data("edit-placeholder"))
this.wire(it, it.element, container);
return it;
}
},
twentyc.cls.Registry
));
twentyc.editable.input.register(
"base",
{
action_on_enter : false,
set : function(value) {
if(value == undefined) {
this.element.val(this.source.text().trim());
} else
this.element.val(value);
},
get : function() {
return this.element.val();
},
changed : function() {
return (this.original_value != this.get());
},
export : function() {
return this.get()
},
make : function() {
return $('<input type="text"></input>');
},
blank : function() {
return (this.element.val() === "");
},
validate : function() {
return true;
},
validation_message : function() {
return gettext("Invalid value") ///
},
required_message : function() {
return gettext("Input required") ///
},
load : function() {
return;
},
apply : function(value) {
if(!this.source.data("edit-template")) {
this.source.text(this.get());
this.source.data("edit-value", this.get());
if(this.static_elements) {
this.static_elements.appendTo(this.source)
}
} else {
var tmplId = this.source.data("edit-template");
var tmpl = twentyc.editable.templates.get(tmplId);
var node = tmpl.clone(true);
if(this.template_handlers[tmplId]) {
this.template_handlers[tmplId](value, node, this);
}
this.source.empty().append(node);
}
},
show_note : function(txt, classes) {
var note = $('<div class="editable input-note"></div>');
note.text(txt)
note.addClass(classes);
if(this.element.hasClass('input-note-relative'))
note.insertAfter(this.element);
else
note.insertBefore(this.element);
this.note = note;
return note;
},
close_note : function() {
if(this.note) {
this.note.detach();
this.note = null;
}
},
show_validation_error : function(msg) {
this.show_note(msg || this.validation_message(), "validation-error");
this.element.addClass("validation-error");
},
reset : function(resetValue) {
this.close_note();
this.element.removeClass("validation-error");
if(resetValue) {
this.source.data("edit-value", this.reset_value);
this.set(this.reset_value);
}
},
template_handlers : {}
}
);
twentyc.editable.input.register(
"string",
{
action_on_enter : true
},
"base"
);
twentyc.editable.input.register(
"password",
{
make : function() {
return $('<input type="password"></input>');
},
validate : function() {
var conf = this.source.data("edit-confirm-with")
if(conf) {
return (this.container.find('[data-edit-name="'+conf+'"]').data("edit-input-instance").get() == this.get());
} else
return true;
},
validation_message : function() {
return gettext("Needs to match password") ///
}
},
"string"
);
twentyc.editable.input.register(
"email",
{
placeholder : "name@example.com",
validate : function() {
if(this.get() === "")
return true
return this.get().match(/@/);
},
validation_message : function() {
return gettext("Needs to be a valid email address"); ///
},
template_handlers : {
"link" : function(value, node) {
node.attr("href", "mailto:"+value).text(value);
}
}
},
"string"
);
twentyc.editable.input.register(
"url",
{
placeholder : "http://www.example.com",
validate : function() {
var url = this.get()
if(url === "")
return true
if(!url.match(/^[a-zA-Z]+:\/\/.+/)) {
url = "http://"+url;
this.set(url);
}
if(url.match(/\s/))
return false;
return true;
},
validation_message : function() {
return gettext("Needs to be a valid url"); ///
},
template_handlers : {
"link" : function(value, node) {
node.attr("href", value).text(value);
}
}
},
"string"
);
twentyc.editable.input.register(
"number",
{
validate : function() {
return this.element.val().match(/^[\d\.\,-]+$/)
},
validation_message : function() {
return gettext("Needs to be a number") ///
}
},
"string"
);
twentyc.editable.input.register(
"bool",
{
value_to_label : function() {
return (this.element.prop("checked") ? gettext("Yes") : gettext("No")); ///
},
make : function() {
return $('<input class="editable input-note-relative" type="checkbox"></input>');
},
get : function() {
return this.element.prop("checked");
},
set : function(value) {
if(value == true || (typeof value == "string" && value.toLowerCase() == "true"))
this.element.prop("checked", true);
else
this.element.prop("checked", false);
},
required_message : function() {
return "Check required"
},
blank : function() {
return this.get() != true;
},
apply : function(value) {
this.source.data("edit-value", this.get());
if(!this.source.data("edit-template")) {
this.source.text(this.value_to_label());
} else {
var tmplId = this.source.data("edit-template");
var tmpl = twentyc.editable.templates.get(tmplId);
var node = tmpl.clone(true);
if(this.template_handlers[tmplId]) {
this.template_handlers[tmplId](value, node, this);
}
this.source.empty().append(node);
}
}
},
"base"
);
twentyc.editable.input.register(
"text",
{
make : function() {
return $('<textarea></textarea>');
}
},
"base"
);
twentyc.editable.input.register(
"select",
{
make : function() {
var node = $('<select></select>');
if(this.source.data("edit-multiple") == "yes")
node.prop("multiple", true);
if(this.source.data("edit-data"))
node.attr("data-edit-data", this.source.data("edit-data"))
return node;
},
set : function() {
var dataId, me = this;
if(dataId=this.source.data("edit-data")) {
twentyc.data.load(dataId, {
callback : function(payload) {
me.load(payload.data);
me.original_value = me.get()
}
});
}
},
value_to_label : function() {
return this.element.children('option:selected').text();
},
apply : function(value) {
this.source.data("edit-value", this.get());
this.source.text(this.value_to_label());
},
add_opt : function(id, name) {
var opt = $('<option></option>');
opt.val(id);
opt.text(name);
var value = ""+this.source.data("edit-value")
if(this.source.data("edit-multiple") == "yes") {
if(value && $.inArray(""+id, value.split(",")) > -1)
opt.prop("selected", true);
} else {
if(id == value)
opt.prop("selected", true);
}
this.element.append(opt);
},
load : function(data) {
var k, v, opt;
this.element.empty();
if(this.source.data("edit-data-all-entry")) {
var allEntry = this.source.data("edit-data-all-entry").split(":")
this.add_opt(allEntry[0], allEntry[1]);
}
for(k in data) {
v = data[k];
this.add_opt(v.id, v.name);
}
this.element.trigger("change");
}
},
"base"
);
/**
* class that managed DOM templates
*
* @class templates
* @namespace twentyc.editable.templates
* @static
*/
twentyc.editable.templates = {
_templates : {},
register : function(id, node) {
if(this._templates[id])
throw("Duplicate template id: "+id);
this._templates[id] = node;
},
get : function(id) {
if(!this._templates[id])
throw("Tried to retrieve unknown template: "+id);
return this._templates[id];
},
copy : function(id) {
return this.get(id).clone().attr("id", null);
},
copy_and_replace : function(id, data, setEditValue) {
var k, tmpl=this.copy(id);
for(k in data) {
tmpl.find('[data-edit-name="'+k+'"]').each(function() {
$(this).text(data[k]);
if(setEditValue)
$(this).data("edit-value", data[k])
});
}
return tmpl;
},
init : function() {
if(this.initialized)
return;
$('#editable-templates, .editable-templates').children().each(function(idx) {
twentyc.editable.templates.register(
this.id,
$(this)
);
});
this.initialized = true;
}
}
twentyc.editable.templates.register("link", $('<a></a>'));
/*
* jQuery functions
*/
$.fn.editable = function(action, arg, dbg) {
/******************************************************************************
* FILTERS
*/
if(action == "filter") {
// filter jquery result
var matched = [];
if(arg) {
// only proceed if arguments are provided
var i = 0,
l = this.length,
input,
node,
nodes,
closest,
result
// BELONGS (container), shortcut for first_closest:["data-edit-target", target]
if(arg.belongs) {
arg.first_closest = ["[data-edit-target], [data-edit-component]", arg.belongs]
}
// FIRST CLOSEST, first_closest:[selector, result]
if(arg.first_closest) {
for(; i < l; i++) {
closest = $(this[i]).parent().closest(arg.first_closest[0]);
if(closest.length && closest.get(0) == arg.first_closest[1].get(0))
matched.push(this[i])
}
}
// GROUPED
else if(arg.grouped) {
for(; i < l; i++) {
node = $(this[i]);
if(node.data("edit-group"))
continue;
nodes = $('[data-edit-group]').each(function(idx) {
var other = $($(this).data("edit-group"));
if(other.get(0) == node.get(0))
matched.push(this);
});
}
}
// CHANGED FIELDS
else if(arg == "changed") {
for(; i < l; i++) {
node = $(this[i]);
input = node.data("edit-input-instance")
if(input && input.changed()) {
matched.push(this[i])
}
}
}
}
return this.pushStack(matched);
} else if(action == "export-fields") {
// track validation errors in here
var validationErrors = {};
arg["_valid"] = true;
// collect values from editable fields
this.each(function(idx) {
try {
$(this).editable("export", arg)
} catch(error) {
if(error.type == "ValidationError") {
validationErrors[error.field] = error.message;
arg["_valid"] = false
} else {
throw(error);
}
}
});
arg["_validationErrors"] = validationErrors;
if(!arg["_valid"]) {
throw({type:"ValidationErrors", data:arg});
}
} else if(action == "collect-payload") {
this.find(".payload").children('[data-edit-name]').each(function(idx) {
var plel = $(this);
arg[plel.data("edit-name")] = plel.text().trim();
});
}
/******************************************************************************
* ACTIONS
*/
this.each(function(idx) {
var me = $(this);
var hasTarget = (me.data("edit-target") != null);
var isComponent = (me.data("edit-component") != null);
var isContainer = (hasTarget || isComponent);
var hasAction = (me.data("edit-action") != null);
var hasType = (me.data("edit-type") != null);
/****************************************************************************
* INIT
**/
if(!action && !me.data("edit-initialized")) {
// mark as initialized so there is no duplicate init
me.data("edit-initialized", true);
// CONTAINER
if(hasTarget) {
if(me.hasClass("always")) {
me.data("edit-mode", "edit");
me.data("edit-always", true);
} else
me.data("edit-mode", "view");
me.editable("sync");
// create error message container
var errorContainer = $('<div class="editable popin error"><div class="main"></div><div class="extra"></div></div>');
errorContainer.hide();
me.prepend(errorContainer)
me.data("edit-error-container", errorContainer);
// create loading shim
var loadingShim = $('<div class="editable loading-shim"></div>');
loadingShim.hide();
me.prepend(loadingShim)
me.data("edit-loading-shim", loadingShim);
// whenever an action signals an error we want to update and show
// the error container
me.on("action-error", function(e, payload) {
var popin = $(this).find(".editable.popin.error").editable("filter", { belongs : $(this) });
popin.find('.main').html(twentyc.editable.error.humanize(payload.reason));
popin.find('.extra').html(payload.info || "");
popin.show();
return false;
});
}
// INTERACTIVE ELEMENT
if(hasAction) {
me.data("edit-parent", arg);
var eventName = "click"
if(
me.data("edit-type") == "bool" ||
me.data("edit-type") == "list" ||
me.data("edit-type") == "select"
)
{
eventName = "change";
}
// bind action event
me.on(eventName, function() {
var handler, a = $(this).data("edit-action");
var container = $(this).closest("[data-edit-target]");
/*
if(!twentyc.editable.action.has(a)) {
if(container.data("edit-module")) {
handler = twentyc.editable.module.instantiate(container);
}
if(!handler)
throw("Unknown action: " + a);
} else
handler = new (twentyc.editable.action.get(a));
*/
if(container.data("edit-module")) {
handler = twentyc.editable.module.instantiate(container);
}
if(!handler)
handler = new (twentyc.editable.action.get(a));
var r = handler.execute($(this), container);
me.trigger("action:"+a, r);
});
me.data("edit-parent", arg);
}
// EDITABLE ELEMENT
if(hasType) {
// editable element
me.data("edit-parent", arg);
}
}
/****************************************************************************
* RESET FORM
*/
else if(action == "reset") {
me.find("[data-edit-type]").
editable("filter", { belongs : me }).
each(function(idx) {
$(this).data("edit-input-instance").reset(true);
});
me.editable("filter", {grouped:true}).not("[data-edit-module]").editable("reset");
me.find("[data-edit-module]").editable("filter", { belongs : me }).editable("reset");
me.find("[data-edit-component]").editable("filter", { belongs : me }).editable("reset");
}
/****************************************************************************
* SYNC
**/
else if(action == "sync") {
var mode = me.data("edit-mode") || "view";
// init contained interactive elements
me.find("[data-edit-action]").
filter("a, input, select").
editable("filter", {belongs:me}).
each(function(idx) {
var child = $(this);
if(!child.data("edit-parent"))
child.editable(null, me)
});
// init contained editable elements
me.find("[data-edit-type]").
editable("filter", { belongs : me }).
each(function(idx) {
var child = $(this);
if(!child.data("edit-parent"))
child.editable(null, me)
if((child.data("edit-mode")||"view") != mode) {
child.editable("toggle");
}
});
// load required data-sets
me.find('[data-edit-data]').
editable('filter', { belongs : me }).
each(function(idx) {
var dataId = $(this).data("edit-data");
twentyc.data.load(dataId);
});
// toggle mode-toggled content
me.find('[data-edit-toggled]').
editable('filter', { belongs : me }).
each(function(idx) {
var child = $(this);
if(child.data("edit-toggled") != mode)
child.hide()
else
child.show()
});
// sync components
me.find('[data-edit-component]').editable("filter", { belongs : me }).each(function() {
var comp = $(this);
comp.data("edit-mode", mode);
comp.editable("sync");
});
}
/****************************************************************************
* TOGGLE
**/
else if(action == "toggle") {
// toggle edit mode on or off
var mode = me.data("edit-mode")
if(me.hasClass("always"))
return;
if(isContainer) {
// CONTAINER
if(mode == "edit") {
me.find('[data-edit-toggled="edit"]').editable("filter", { belongs : me }).hide();
me.find('[data-edit-toggled="view"]').editable("filter", { belongs : me }).show();
mode = "view";
me.removeClass("mode-edit")
if(!arg)
me.trigger("edit-cancel");
} else {
me.find('[data-edit-toggled="edit"]').editable("filter", { belongs : me }).show();
me.find('[data-edit-toggled="view"]').editable("filter", { belongs : me }).hide();
mode = "edit";
me.addClass("mode-edit")
}
// hide pop-ins
me.find('.editable.popin').editable("filter", { belongs : me }).hide();
// toggled editable elements
me.find("[data-edit-type], [data-edit-component]").editable("filter", { belongs : me }).editable("toggle", arg);
// toggle other containers that are flagged to be toggled by this container
me.editable("filter", { grouped : 1 }).each(function(idx) {
$(this).editable("toggle", arg);
});
} else if(hasType) {
// EDITABLE ELEMENT
var input;
if(me.data("edit-always"))
return;
if(mode == "edit") {
// element is currently editable, switch it back to view-only
// mode
input = me.data("edit-input-instance")
input.reset();
if(arg && !$.isEmptyObject(arg.data))
input.apply(arg.data[me.data("edit-name")])
else
me.html(me.data("edit-content-backup"))
me.data("edit-input-instance", null);
mode = "view";
} else {
// element is currently not editable, switch it to edit mode
input = twentyc.editable.input.create(
me.data('edit-type'),
me,
me.closest("[data-edit-target]")
);
input.element.data('edit-input', input);
input.element.data('edit-name', me.data('edit-name'));
input.element.data('edit-type', me.data('edit-type'));
input.element.addClass("editable "+ me.data("edit-type"));
// store old content so we can switch back to it
// in case of edit-cancel event
me.data("edit-content-backup", me.html());
// replace content with input
me.data("edit-input-instance", input);
me.empty();
me.append(input.frame);
mode = "edit";
}
}
me.data("edit-mode", mode);
me.trigger("toggle", [mode]);
}
/****************************************************************************
* TOGGLE LOADING SHIM
**/
else if(action == "loading-shim") {
if(arg == "show" || arg == "hide") {
me.children(".editable.loading-shim")[arg]();
}
}
/****************************************************************************
* REMOVE ERROR POPINS
*/
else if(action == "clear-error-popins") {
me.find('.editable.popin').editable("filter", { belongs : me }).hide();
}
/****************************************************************************
* ACCEPT VALUES
* This sets the original_values of all input instances within the container
* to the current input value
*/
else if(action == "accept-values") {
me.find("[data-edit-type]").editable("filter", { belongs : me }).each(function() {
var input = $(this).data("edit-input-instance");
if(input)
input.original_value = input.get();
});
}
/****************************************************************************
* ADD PAYLOAD
**/
else if(action == "payload") {
var payload = me.children(".payload")
if(!payload.length) {
payload = $('<div></div>')
payload.addClass("editable");
payload.addClass("payload");
me.prepend(payload);
}
var i, node;
for(i in arg) {
node = payload.children('[data-edit-name="'+i+'"]')
if(!node.length) {
node = $('<div></div>')
node.attr("data-edit-name", i)
payload.append(node);
}
node.text(arg[i]);
}
}
/****************************************************************************
* EXPORT FORM DATA
**/
else if(action == "export") {
// export form data to object literal
if(isContainer) {
// container, find all inputs within, exit if not in edit mode
if(me.data("edit-mode") != "edit" && !me.hasClass("always"))
return;
// export all the fields that belong to this container
me.find('[data-edit-type]').
editable("filter", { belongs : me }).
editable("export-fields", arg);
// if data-edit-id is specified make sure to copy it to exported data
// under _id
if(me.data("edit-id") != undefined) {
arg["_id"] = me.data("edit-id");
}
// check if payload element exists, and if it does add the data from
// it to the exported data
me.editable("collect-payload", arg);
me.trigger("export", [arg])
} else if(hasType) {
// editable element, see if input element exists and retrieve value
var input;
if(input=me.data("edit-input-instance")) {
//why would this be here? breaks certain form submissions
//by doing a premature reset - taking it out does not break any tests
//input.reset();
// if input is required make sure it is not blank
if(me.data("edit-required") == "yes") {
if(input.blank()) {
input.show_validation_error(input.required_message());
throw({type:"ValidationError", field:me.data("edit-name"), message:input.required_message()})
}
}
// validate input
if(!input.validate()) {
input.show_validation_error();
throw({type:"ValidationError", field:me.data("edit-name"), message:input.validation_message()})
}
arg[me.data("edit-name")] = input.export();
if(typeof arg["_changed"] == "undefined") {
arg["_changed"] = input.changed() ? 1: 0;
} else {
arg["_changed"] += input.changed() ? 1 : 0;
}
}
}
}
});
};
/*
* Init
*/
$(document).ready(function() {
twentyc.editable.init();
});
})(jQuery);