openerp.web.form = function (instance) { var _t = instance.web._t, _lt = instance.web._lt; var QWeb = instance.web.qweb; /** @namespace */ instance.web.form = {}; /** * Interface implemented by the form view or any other object * able to provide the features necessary for the fields to work. * * Properties: * - display_invalid_fields : if true, all fields where is_valid() return true should * be displayed as invalid. * Events: * - view_content_has_changed : when the values of the fields have changed. When * this event is triggered all fields should reprocess their modifiers. */ instance.web.form.FieldManagerMixin = { /** * Must return the asked field as in fields_get. */ get_field: function(field_name) {}, /** * Called by the field when the translate button is clicked. */ open_translate_dialog: function(field) {}, /** * Returns true when the view is in create mode. */ is_create_mode: function() {}, }; instance.web.views.add('form', 'instance.web.FormView'); /** * Properties: * - actual_mode: always "view", "edit" or "create". Read-only property. Determines * the mode used by the view. */ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerMixin, { /** * Indicates that this view is not searchable, and thus that no search * view should be displayed (if there is one active). */ searchable: false, template: "FormView", display_name: _lt('Form'), view_type: "form", /** * @constructs instance.web.FormView * @extends instance.web.View * * @param {instance.web.Session} session the current openerp session * @param {instance.web.DataSet} dataset the dataset this view will work with * @param {String} view_id the identifier of the OpenERP view object * @param {Object} options * - resize_textareas : [true|false|max_height] * * @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance */ init: function(parent, dataset, view_id, options) { var self = this; this._super(parent); this.set_default_options(options); this.dataset = dataset; this.model = dataset.model; this.view_id = view_id || false; this.fields_view = {}; this.fields = {}; this.fields_order = []; this.datarecord = {}; this.default_focus_field = null; this.default_focus_button = null; this.fields_registry = instance.web.form.widgets; this.tags_registry = instance.web.form.tags; this.has_been_loaded = $.Deferred(); this.translatable_fields = []; _.defaults(this.options, { "not_interactible_on_create": false, "initial_mode": "view", "disable_autofocus": false, }); this.is_initialized = $.Deferred(); this.mutating_mutex = new $.Mutex(); this.on_change_mutex = new $.Mutex(); this.reload_mutex = new $.Mutex(); this.__clicked_inside = false; this.__blur_timeout = null; this.rendering_engine = new instance.web.form.FormRenderingEngine(this); this.qweb = null; // A QWeb instance will be created if the view is a QWeb template self.set({actual_mode: self.options.initial_mode}); this.has_been_loaded.then(function() { self.on("change:actual_mode", self, self.check_actual_mode); self.check_actual_mode(); self.on("change:actual_mode", self, self.init_pager); self.init_pager(); }); }, destroy: function() { _.each(this.get_widgets(), function(w) { w.off('focused blurred'); w.destroy(); }); if (this.$element) { this.$element.off('.formBlur'); } this._super(); }, on_loaded: function(data) { var self = this; if (!data) { throw new Error("No data provided."); } if (this.arch) { throw "Form view does not support multiple calls to on_loaded"; } this.fields_order = []; this.fields_view = data; this.rendering_engine.set_fields_registry(this.fields_registry); this.rendering_engine.set_tags_registry(this.tags_registry); if (!this.extract_qweb_template(data)) { this.rendering_engine.set_fields_view(data); var $dest = this.$element.hasClass("oe_form_container") ? this.$element : this.$element.find('.oe_form_container'); this.rendering_engine.render_to($dest); } this.$element.on('mousedown.formBlur', function () { self.__clicked_inside = true; }); this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self})); if (this.options.$buttons) { this.$buttons.appendTo(this.options.$buttons); } else { this.$element.find('.oe_form_buttons').replaceWith(this.$buttons); } this.$buttons.on('click','.oe_form_button_create',this.on_button_create); this.$buttons.on('click','.oe_form_button_edit',this.on_button_edit); this.$buttons.on('click','.oe_form_button_save',this.on_button_save); this.$buttons.on('click','.oe_form_button_cancel',this.on_button_cancel); this.$sidebar = this.options.$sidebar || this.$element.find('.oe_form_sidebar'); if (!this.sidebar && this.options.$sidebar) { this.sidebar = new instance.web.Sidebar(this); this.sidebar.appendTo(this.$sidebar); if(this.fields_view.toolbar) { this.sidebar.add_toolbar(this.fields_view.toolbar); } this.sidebar.add_items('other', [ { label: _t('Delete'), callback: self.on_button_delete }, { label: _t('Duplicate'), callback: self.on_button_duplicate }, { label: _t('Set Default'), callback: function (item) { self.open_defaults_dialog(); } } ]); } this.has_been_loaded.resolve(); // Add bounce effect on button 'Edit' when click on readonly page view. this.$element.find(".oe_form_field,label").on('click', function (e) { if(self.get("actual_mode") == "view") { var $button = self.options.$buttons.find(".oe_form_button_edit"); $button.effect('bounce', {distance: 18, times: 5}, 150) } }); return $.when(); }, extract_qweb_template: function(fvg) { for (var i=0, ii=fvg.arch.children.length; i < ii; i++) { var child = fvg.arch.children[i]; if (child.tag === "templates") { this.qweb = new QWeb2.Engine(); this.qweb.add_template(instance.web.json_node_to_xml(child)); if (!this.qweb.has_template('form')) { throw new Error("No QWeb template found for form view"); } return true; } } this.qweb = null; return false; }, get_fvg_from_qweb: function(record) { var view = this.qweb.render('form', this.get_qweb_context(record)); var fvg = _.clone(this.fields_view); fvg.arch = instance.web.xml_to_json(instance.web.str_to_xml(view).firstChild); return fvg; }, get_qweb_context: function(record) { var self = this, new_record = {}; _.each(record, function(value_, name) { var r = _.clone(self.fields_view.fields[name] || {}); if ((r.type === 'date' || r.type === 'datetime') && value_) { r.raw_value = instance.web.auto_str_to_date(value_); } else { r.raw_value = value_; } r.value = instance.web.format_value(value_, r); new_record[name] = r; }); return { record : new_record, new_record : !record.id }; }, kill_current_form: function() { _.each(this.getChildren(), function(el) { el.destroy(); }); this.fields = {}; this.fields_order = []; this.default_focus_field = null; this.default_focus_button = null; this.translatable_fields = []; this.$element.find('.oe_form_container').empty(); }, widgetFocused: function() { // Clear click flag if used to focus a widget this.__clicked_inside = false; if (this.__blur_timeout) { clearTimeout(this.__blur_timeout); this.__blur_timeout = null; } }, widgetBlurred: function() { if (this.__clicked_inside) { // clicked in an other section of the form (than the currently // focused widget) => just ignore the blurring entirely? this.__clicked_inside = false; return; } var self = this; // clear timeout, if any this.widgetFocused(); this.__blur_timeout = setTimeout(function () { self.trigger('blurred'); }, 0); }, do_load_state: function(state, warm) { if (state.id && this.datarecord.id != state.id) { if (!this.dataset.get_id_index(state.id)) { this.dataset.ids.push(state.id); } this.dataset.select_id(state.id); if (warm) { this.do_show(); } } }, /** * * @param {Object} [options] * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view". * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record * @return {$.Deferred} */ do_show: function (options) { var self = this; options = options || {}; if (this.sidebar) { this.sidebar.$element.show(); } if (this.$buttons) { this.$buttons.show(); } if (this.$pager) { this.$pager.show(); } this.$element.show().css({ opacity: '0', filter: 'alpha(opacity = 0)' }); this.$element.add(this.$buttons).removeClass('oe_form_dirty'); var shown = this.has_been_loaded; if (options.reload !== false) { shown = shown.pipe(function() { if (self.dataset.index === null) { // null index means we should start a new record return self.on_button_new(); } var fields = _.keys(self.fields_view.fields); fields.push('display_name'); return self.dataset.read_index(fields, { context: { 'bin_size': true, 'future_display_name' : true } }).pipe(self.on_record_loaded); }); } return shown.pipe(function() { self._actualize_mode(options.mode || self.options.initial_mode); self.$element.css({ opacity: '1', filter: 'alpha(opacity = 100)' }); }); }, do_hide: function () { if (this.sidebar) { this.sidebar.$element.hide(); } if (this.$buttons) { this.$buttons.hide(); } if (this.$pager) { this.$pager.hide(); } this._super(); }, on_record_loaded: function(record) { var self = this, set_values = []; if (!record) { this.set({ 'title' : undefined }); this.do_warn("Form", "The record could not be found in the database.", true); return $.Deferred().reject(); } this.datarecord = record; this._actualize_mode(); this.set({ 'title' : record.id ? record.display_name : "New record" }); if (this.qweb) { this.kill_current_form(); this.rendering_engine.set_fields_view(this.get_fvg_from_qweb(record)); var $dest = this.$element.hasClass("oe_form_container") ? this.$element : this.$element.find('.oe_form_container'); this.rendering_engine.render_to($dest); } _(this.fields).each(function (field, f) { field._dirty_flag = false; var result = field.set_value(self.datarecord[f] || false); set_values.push(result); }); return $.when.apply(null, set_values).pipe(function() { if (!record.id) { // New record: Second pass in order to trigger the onchanges // respecting the fields order defined in the view _.each(self.fields_order, function(field_name) { if (record[field_name] !== undefined) { var field = self.fields[field_name]; field._dirty_flag = true; self.do_onchange(field); } }); } self.on_form_changed(); self.is_initialized.resolve(); self.do_update_pager(record.id == null); if (self.sidebar) { self.sidebar.do_attachement_update(self.dataset, self.datarecord.id); } if (record.id) { self.do_push_state({id:record.id}); } else { self.do_push_state({}); } self.$element.add(self.$buttons).removeClass('oe_form_dirty'); self.autofocus(); }); }, /** * Loads and sets up the default values for the model as the current * record * * @return {$.Deferred} */ load_defaults: function () { var keys = _.keys(this.fields_view.fields); if (keys.length) { return this.dataset.default_get(keys) .pipe(this.on_record_loaded); } return this.on_record_loaded({}); }, on_form_changed: function() { this.trigger("view_content_has_changed"); }, do_notify_change: function() { this.$element.add(this.$buttons).addClass('oe_form_dirty'); }, on_pager_action: function(action) { if (this.can_be_discarded()) { switch (action) { case 'first': this.dataset.index = 0; break; case 'previous': this.dataset.previous(); break; case 'next': this.dataset.next(); break; case 'last': this.dataset.index = this.dataset.ids.length - 1; break; } this.reload(); } }, init_pager: function() { var self = this; if (this.$pager) this.$pager.remove(); if (this.get("actual_mode") === "create") return; this.$pager = $(QWeb.render("FormView.pager", {'widget':self})); if (this.options.$pager) { this.$pager.appendTo(this.options.$pager); } else { this.$element.find('.oe_form_pager').replaceWith(this.$pager); } this.$pager.on('click','a[data-pager-action]',function() { var action = $(this).data('pager-action'); self.on_pager_action(action); }); this.do_update_pager(); }, do_update_pager: function(hide_index) { var index = hide_index ? '-' : this.dataset.index + 1; this.$pager.find('button').prop('disabled', this.dataset.ids.length < 2).end() .find('span.oe_pager_index').html(index).end() .find('span.oe_pager_count').html(this.dataset.ids.length); }, parse_on_change: function (on_change, widget) { var self = this; var onchange = _.str.trim(on_change); var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/); if (!call) { return null; } var method = call[1]; if (!_.str.trim(call[2])) { return {method: method, args: [], context_index: null} } var argument_replacement = { 'False': function () {return false;}, 'True': function () {return true;}, 'None': function () {return null;}, 'context': function (i) { context_index = i; var ctx = new instance.web.CompoundContext(self.dataset.get_context(), widget.build_context() ? widget.build_context() : {}); return ctx; } }; var parent_fields = null, context_index = null; var args = _.map(call[2].split(','), function (a, i) { var field = _.str.trim(a); // literal constant or context if (field in argument_replacement) { return argument_replacement[field](i); } // literal number if (/^-?\d+(\.\d+)?$/.test(field)) { return Number(field); } // form field if (self.fields[field]) { var value_ = self.fields[field].get_value(); return value_ == null ? false : value_; } // parent field var splitted = field.split('.'); if (splitted.length > 1 && _.str.trim(splitted[0]) === "parent" && self.dataset.parent_view) { if (parent_fields === null) { parent_fields = self.dataset.parent_view.get_fields_values([self.dataset.child_name]); } var p_val = parent_fields[_.str.trim(splitted[1])]; if (p_val !== undefined) { return p_val == null ? false : p_val; } } // string literal var first_char = field[0], last_char = field[field.length-1]; if ((first_char === '"' && last_char === '"') || (first_char === "'" && last_char === "'")) { return field.slice(1, -1); } throw new Error("Could not get field with name '" + field + "' for onchange '" + onchange + "'"); }); return { method: method, args: args, context_index: context_index }; }, do_onchange: function(widget, processed) { var self = this; return this.on_change_mutex.exec(function() { try { var response = {}, can_process_onchange = $.Deferred(); processed = processed || []; processed.push(widget.name); var on_change = widget.node.attrs.on_change; if (on_change) { var change_spec = self.parse_on_change(on_change, widget); if (change_spec) { var ajax = { url: '/web/dataset/onchange', async: false }; can_process_onchange = self.rpc(ajax, { model: self.dataset.model, method: change_spec.method, args: [(self.datarecord.id == null ? [] : [self.datarecord.id])].concat(change_spec.args), context_id: change_spec.context_index == undefined ? null : change_spec.context_index + 1 }).then(function(r) { _.extend(response, r); }); } else { console.warn("Wrong on_change format", on_change); } } // fail if onchange failed if (can_process_onchange.isRejected()) { return can_process_onchange; } if (widget.field['change_default']) { var fieldname = widget.name, value_; if (response.value && (fieldname in response.value)) { // Use value from onchange if onchange executed value_ = response.value[fieldname]; } else { // otherwise get form value for field value_ = self.fields[fieldname].get_value(); } var condition = fieldname + '=' + value_; if (value_) { can_process_onchange = self.rpc({ url: '/web/dataset/call', async: false }, { model: 'ir.values', method: 'get_defaults', args: [self.model, condition] }).then(function (results) { if (!results.length) { return; } if (!response.value) { response.value = {}; } for(var i=0; i%s', _.escape(f.string)); }).value(); warnings.unshift(''); this.do_warn("The following fields are invalid :", warnings.join('')); }, on_saved: function(r, success) { if (!r.result) { // should not happen in the server, but may happen for internal purpose return $.Deferred().reject(); } else { return $.when(this.reload()).pipe(function () { return r; }) .then(success); } }, /** * Updates the form' dataset to contain the new record: * * * Adds the newly created record to the current dataset (at the end by * default) * * Selects that record (sets the dataset's index to point to the new * record's id). * * Updates the pager and sidebar displays * * @param {Object} r * @param {Function} success callback to execute after having updated the dataset * @param {Boolean} [prepend_on_create=false] adds the newly created record at the beginning of the dataset instead of the end */ on_created: function(r, success, prepend_on_create) { if (!r.result) { // should not happen in the server, but may happen for internal purpose return $.Deferred().reject(); } else { this.datarecord.id = r.result; if (!prepend_on_create) { this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id])); this.dataset.index = this.dataset.ids.length - 1; } else { this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids)); this.dataset.index = 0; } this.do_update_pager(); if (this.sidebar) { this.sidebar.do_attachement_update(this.dataset, this.datarecord.id); } //openerp.log("The record has been created with id #" + this.datarecord.id); return $.when(this.reload()).pipe(function () { return _.extend(r, {created: true}); }) .then(success); } }, on_action: function (action) { console.debug('Executing action', action); }, reload: function() { var self = this; return this.reload_mutex.exec(function() { if (self.dataset.index == null) { self.do_prev_view(); return $.Deferred().reject().promise(); } if (self.dataset.index == null || self.dataset.index < 0) { return $.when(self.on_button_new()); } else { var fields = _.keys(self.fields_view.fields); fields.push('display_name'); return self.dataset.read_index(fields, { context : { 'bin_size' : true, 'future_display_name' : true } }).pipe(self.on_record_loaded); } }); }, get_widgets: function() { return _.filter(this.getChildren(), function(obj) { return obj instanceof instance.web.form.FormWidget; }); }, get_fields_values: function(blacklist) { blacklist = blacklist || []; var values = {}; var ids = this.get_selected_ids(); values["id"] = ids.length > 0 ? ids[0] : false; _.each(this.fields, function(value_, key) { if (_.include(blacklist, key)) { return; } values[key] = value_.get_value(); }); return values; }, get_selected_ids: function() { var id = this.dataset.ids[this.dataset.index]; return id ? [id] : []; }, recursive_save: function() { var self = this; return $.when(this.do_save()).pipe(function(res) { if (self.dataset.parent_view) return self.dataset.parent_view.recursive_save(); }); }, is_dirty: function() { return _.any(this.fields, function (value_) { return value_._dirty_flag; }); }, is_interactible_record: function() { var id = this.datarecord.id; if (!id) { if (this.options.not_interactible_on_create) return false; } else if (typeof(id) === "string") { if(instance.web.BufferedDataSet.virtual_id_regex.test(id)) return false; } return true; }, sidebar_context: function () { return this.do_save().pipe(_.bind(function() {return this.get_fields_values();}, this)); }, open_defaults_dialog: function () { var self = this; var fields = _.chain(this.fields) .map(function (field, name) { var value = field.get_value(); // ignore fields which are empty, invisible, readonly, o2m // or m2m if (!value || field.get('invisible') || field.get("readonly") || field.field.type === 'one2many' || field.field.type === 'many2many') { return false; } var displayed = value; switch (field.field.type) { case 'selection': displayed = _(field.values).find(function (option) { return option[0] === value; })[1]; break; case 'many2one': displayed = field.get_displayed(); break; } return { name: name, string: field.string, value: value, displayed: displayed, // convert undefined to false change_default: !!field.field.change_default } }) .compact() .sortBy(function (field) { return field.string; }) .value(); var conditions = _.chain(fields) .filter(function (field) { return field.change_default; }) .value(); var d = new instance.web.Dialog(this, { title: _t("Set Default"), args: { fields: fields, conditions: conditions }, buttons: [ {text: _t("Close"), click: function () { d.close(); }}, {text: _t("Save default"), click: function () { var $defaults = d.$element.find('#formview_default_fields'); var field_to_set = $defaults.val(); if (!field_to_set) { $defaults.parent().addClass('oe_form_invalid'); return; } var condition = d.$element.find('#formview_default_conditions').val(), all_users = d.$element.find('#formview_default_all').is(':checked'); new instance.web.DataSet(self, 'ir.values').call( 'set_default', [ self.dataset.model, field_to_set, self.fields[field_to_set].get_value(), all_users, false, condition || false ]).then(function () { d.close(); }); }} ] }); d.template = 'FormView.set_default'; d.open(); }, register_field: function(field, name) { this.fields[name] = field; this.fields_order.push(name); if (JSON.parse(field.node.attrs.default_focus || "0")) { this.default_focus_field = field; } field.on('focused', null, this.proxy('widgetFocused')) .on('blurred', null, this.proxy('widgetBlurred')); if (this.get_field(name).translate) { this.translatable_fields.push(field); } field.on('changed_value', this, function() { field._dirty_flag = true; if (field.is_syntax_valid()) { this.do_onchange(field); this.on_form_changed(true); this.do_notify_change(); } }); }, get_field: function(field_name) { return this.fields_view.fields[field_name]; }, is_create_mode: function() { return this.get("actual_mode") === "create"; }, open_translate_dialog: function(field) { return this._super(field); }, }); /** * Interface to be implemented by rendering engines for the form view. */ instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({ set_fields_view: function(fields_view) {}, set_fields_registry: function(fields_registry) {}, render_to: function($element) {}, }); /** * Default rendering engine for the form view. * * It is necessary to set the view using set_view() before usage. */ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({ init: function(view) { this.view = view; }, set_fields_view: function(fvg) { this.fvg = fvg; this.version = parseFloat(this.fvg.arch.attrs.version); if (isNaN(this.version)) { this.version = 6.1; } }, set_tags_registry: function(tags_registry) { this.tags_registry = tags_registry; }, set_fields_registry: function(fields_registry) { this.fields_registry = fields_registry; }, // Backward compatibility tools, current default version: v6.1 process_version: function() { if (this.version < 7.0) { this.$form.find('form:first').wrapInner(''); this.$form.find('page').each(function() { if (!$(this).parents('field').length) { $(this).wrapInner(''); } }); } }, render_to: function($target) { var self = this; this.$target = $target; // TODO: I know this will save the world and all the kitten for a moment, // but one day, we will have to get rid of xml2json var xml = instance.web.json_node_to_xml(this.fvg.arch); this.$form = $('
' + xml + '
'); this.process_version(); this.fields_to_init = []; this.tags_to_init = []; this.labels = {}; this.process(this.$form); this.$form.appendTo(this.$target); _.each(this.fields_to_init, function($elem) { var name = $elem.attr("name"); if (!self.fvg.fields[name]) { throw new Error("Field '" + name + "' specified in view could not be found."); } var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]); if (!obj) { throw new Error("Widget type '"+ $elem.attr('widget') + "' is not implemented"); } var w = new (obj)(self.view, instance.web.xml_to_json($elem[0])); var $label = self.labels[$elem.attr("name")]; if ($label) { w.set_input_id($label.attr("for")); } self.alter_field(w); self.view.register_field(w, $elem.attr("name")); w.replace($elem); }); _.each(this.tags_to_init, function($elem) { var tag_name = $elem[0].tagName.toLowerCase(); var obj = self.tags_registry.get_object(tag_name); var w = new (obj)(self.view, instance.web.xml_to_json($elem[0])); w.replace($elem); }); // TODO: return a deferred }, render_element: function(template /* dictionaries */) { var dicts = [].slice.call(arguments).slice(1); var dict = _.extend.apply(_, dicts); dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb return $(QWeb.render(template, dict)); }, alter_field: function(field) { }, toggle_layout_debugging: function() { if (!this.$target.has('.oe_layout_debug_cell:first').length) { this.$target.find('[title]').removeAttr('title'); this.$target.find('.oe_form_group_cell').each(function() { var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan'); $(this).attr('title', text); }); } this.$target.toggleClass('oe_layout_debugging'); }, process: function($tag) { var self = this; var tagname = $tag[0].nodeName.toLowerCase(); if (this.tags_registry.contains(tagname)) { this.tags_to_init.push($tag); return $tag; } var fn = self['process_' + tagname]; if (fn) { var args = [].slice.call(arguments); args[0] = $tag; return fn.apply(self, args); } else { // generic tag handling, just process children $tag.children().each(function() { self.process($(this)); }); self.handle_common_properties($tag, $tag); $tag.removeAttr("modifiers"); return $tag; } }, process_sheet: function($sheet) { var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes()); this.handle_common_properties($new_sheet, $sheet); var $dst = $new_sheet.find('.oe_form_sheet'); $sheet.contents().appendTo($dst); $sheet.before($new_sheet).remove(); this.process($new_sheet); }, process_form: function($form) { if ($form.find('> sheet').length === 0) { $form.addClass('oe_form_nosheet'); } var $new_form = this.render_element('FormRenderingForm', $form.getAttributes()); this.handle_common_properties($new_form, $form); $form.contents().appendTo($new_form); if ($form[0] === this.$form[0]) { // If root element, replace it this.$form = $new_form; } else { $form.before($new_form).remove(); } this.process($new_form); }, /* * Used by direct children of a tag only * This method will add the implicit for every field * in the */ preprocess_field: function($field) { var self = this; var name = $field.attr('name'), field_colspan = parseInt($field.attr('colspan'), 10), field_modifiers = JSON.parse($field.attr('modifiers') || '{}'); if ($field.attr('nolabel') === '1') return; $field.attr('nolabel', '1'); var found = false; this.$form.find('label[for="' + name + '"]').each(function(i ,el) { $(el).parents().each(function(unused, tag) { var name = tag.tagName.toLowerCase(); if (name === "field" || name in self.tags_registry.map) found = true; }); }); if (found) return; var $label = $('