wcs/wcs/qommon/static/js/qommon.forms.js

929 lines
34 KiB
JavaScript

String.prototype.similarity = function(string) {
// adapted from https://github.com/jordanthomas/jaro-winkler (licensed as MIT)
var s1 = this, s2 = string;
var m = 0;
var i;
var j;
// Exit early if either are empty.
if (s1.length === 0 || s2.length === 0) {
return 0;
}
// Convert to upper
s1 = s1.toUpperCase();
s2 = s2.toUpperCase();
// Exit early if they're an exact match.
if (s1 === s2) {
return 1;
}
var range = (Math.floor(Math.max(s1.length, s2.length) / 2)) - 1;
var s1Matches = new Array(s1.length);
var s2Matches = new Array(s2.length);
for (i = 0; i < s1.length; i++) {
var low = (i >= range) ? i - range : 0;
var high = (i + range <= (s2.length - 1)) ? (i + range) : (s2.length - 1);
for (j = low; j <= high; j++) {
if (s1Matches[i] !== true && s2Matches[j] !== true && s1[i] === s2[j]) {
++m;
s1Matches[i] = s2Matches[j] = true;
break;
}
}
}
// Exit early if no matches were found.
if (m === 0) {
return 0;
}
// Count the transpositions.
var k = 0;
var numTrans = 0;
for (i = 0; i < s1.length; i++) {
if (s1Matches[i] === true) {
for (j = k; j < s2.length; j++) {
if (s2Matches[j] === true) {
k = j + 1;
break;
}
}
if (s1[i] !== s2[j]) {
++numTrans;
}
}
}
var weight = (m / s1.length + m / s2.length + (m - (numTrans / 2)) / m) / 3;
var l = 0;
var p = 0.1;
if (weight > 0.7) {
while (s1[l] === s2[l] && l < 4) {
++l;
}
weight = weight + l * p * (1 - weight);
}
return weight;
}
/* Make table widget responsive
* new Responsive_table_widget(table)
*/
const Responsive_table_widget = function (table) {
'use strict';
this.table = table;
this.col_headers = table.querySelectorAll('thead th');
this.col_headers_text = [];
this.body_rows = table.querySelectorAll('tbody tr');
this.parent = table.parentElement;
this.init();
};
Responsive_table_widget.prototype.storeHeaders = function () {
'use strict';
let _self = this;
$(this.col_headers).each(function (i, header) {
_self.col_headers_text.push(header.innerText);
});
$(this.body_rows).each(function (i, tr) {
$(tr.querySelectorAll('td')).each(function (i, td) {
td.dataset.colHeader = _self.col_headers_text[i];
});
});
};
Responsive_table_widget.prototype.fit = function () {
'use strict';
if (this.parent.clientWidth < this.table.clientWidth) {
this.table.style.width = "100%";
} else if (! $(this.parent).parent().is('[class*=" grid-"]')) {
this.table.style.width = "auto";
}
};
Responsive_table_widget.prototype.init = function () {
'use strict';
let _self = this;
this.table.classList.add('responsive-tableWidget');
this.storeHeaders();
this.fit();
// debounce resize event
let callback;
window.addEventListener("resize", function () {
clearTimeout(callback);
callback = setTimeout( function () {
_self.fit.call(_self)
}, 200);
});
};
$(function() {
$('.section.foldable > h2 [role=button]').each(function() {
$(this).attr('tabindex', '0');
});
$('.section.foldable > h2 [role=button]').on('keydown', function(ev) {
if (ev.keyCode == 13 || ev.keyCode == 32) { // enter || space
$(this).trigger('click');
return false;
}
});
$('.section.foldable > h2').off('click').click(function() {
var folded = $(this).parent().hasClass('folded');
var $button = $(this).find('[role=button]').first();
if ($button.length) {
$button[0].setAttribute('aria-expanded', `${folded}`);
}
$(this).parent().toggleClass('folded');
$(this).parent().find('.qommon-map').trigger('qommon:invalidate');
});
var autosave_timeout_id = null;
var autosave_is_running = false;
var autosave_button_to_click_on_complete = null;
if ($('form[data-has-draft]:not([data-autosave=false])').length == 1) {
var last_auto_save = $('form[data-has-draft]').serialize();
var error_counter = 0;
function autosave() {
var $form = $('form[data-has-draft]');
if ($form.hasClass('disabled-during-submit')) return;
var new_auto_save = $form.serialize();
if (last_auto_save == new_auto_save) {
install_autosave();
return;
}
autosave_is_running = true;
$.ajax({
type: 'POST',
url: window.location.pathname + 'autosave',
data: new_auto_save,
success: function(json) {
if (json.result == 'success') {
error_counter = -1;
last_auto_save = new_auto_save;
}
},
complete: function() {
error_counter++;
if (error_counter > 5) {
// stop trying to autosave unless there are new changes
last_auto_save = new_auto_save;
}
autosave_is_running = false;
if (autosave_timeout_id !== null) {
install_autosave();
}
if (autosave_button_to_click_on_complete !== null) {
autosave_button_to_click_on_complete.click();
}
}
});
}
function install_autosave() {
// debounce
window.clearTimeout(autosave_timeout_id);
autosave_timeout_id = window.setTimeout(autosave, 5000);
}
$(document).on('mouseover scroll keydown', function() {
if (autosave_timeout_id !== null && ! autosave_is_running) {
install_autosave();
}
});
$(window).on('pagehide', function () {
if (autosave_timeout_id !== null && ! $('body').hasClass('autosaving')) {
window.clearTimeout(autosave_timeout_id);
autosave_timeout_id = null;
autosave();
}
});
$(document).on('visibilitychange', function () {
if (document.visibilityState == 'hidden' && autosave_timeout_id !== null && ! $('body').hasClass('autosaving')) {
window.clearTimeout(autosave_timeout_id);
autosave_timeout_id = null;
autosave();
}
});
install_autosave();
$('#tracking-code a').on('click', autosave);
$(document).on('wcs:set-last-auto-save', function() {
last_auto_save = $('form[data-has-draft]').serialize();
});
}
// common domains we want to offer suggestions for.
var well_known_domains = Array();
// existing domains we know but don't want to use in suggestion engine.
var known_domains = Array();
if (typeof WCS_WELL_KNOWN_DOMAINS !== 'undefined') {
var well_known_domains = WCS_WELL_KNOWN_DOMAINS;
var known_domains = WCS_VALID_KNOWN_DOMAINS;
}
function add_js_behaviours($base) {
$base.find('input[type=email]').on('change wcs:change', function() {
var $email_input = $(this);
var val = $email_input.val();
var val_domain = val.split('@')[1];
var $domain_hint_div = this.domain_hint_div;
var highest_ratio = 0;
var suggestion = null;
if (typeof val_domain === 'undefined' || known_domains.indexOf(val_domain) > -1) {
// domain not yet typed in, or known domain, don't suggest anything.
if ($domain_hint_div) {
$domain_hint_div.hide();
}
return;
}
for (var i=0; i < well_known_domains.length; i++) {
var domain = well_known_domains[i];
var ratio = val_domain.similarity(domain);
if (ratio > highest_ratio) {
highest_ratio = ratio;
suggestion = domain;
}
}
if (highest_ratio > 0.80 && highest_ratio < 1) {
if ($domain_hint_div === undefined) {
$domain_hint_div = $('<div class="field-live-hint"><p class="message"></p><button type="button" class="action"></button><button type="button" class="close"><span class="sr-only"></span></button></div>');
this.domain_hint_div = $domain_hint_div;
$(this).after($domain_hint_div);
$domain_hint_div.find('button.action').on('click', function() {
$email_input.val($email_input.val().replace(/@.*/, '@' + $(this).data('suggestion')));
$email_input.trigger('wcs:change');
$domain_hint_div.hide();
return false;
});
$domain_hint_div.find('button.close').on('click', function() {
$domain_hint_div.hide();
return false;
});
}
$domain_hint_div.find('p').text(WCS_I18N.email_domain_suggest + ' @' + suggestion + ' ?');
$domain_hint_div.find('button.action').text(WCS_I18N.email_domain_fix);
$domain_hint_div.find('button.action').data('suggestion', suggestion);
$domain_hint_div.find('button.close span.sr-only').text(WCS_I18N.close);
$domain_hint_div.show();
} else if ($domain_hint_div) {
$domain_hint_div.hide();
}
});
$base.find('.date-pick').each(function() {
if (this.type == "date" || this.type == "time") {
return; // prefer native date/time widgets
}
var $date_input = $(this);
$date_input.attr('type', 'text');
if ($date_input.data('formatted-value')) {
$date_input.val($date_input.data('formatted-value'));
}
var options = Object();
options.autoclose = true;
options.weekStart = 1;
options.format = $date_input.data('date-format');
options.minView = $date_input.data('min-view');
options.maxView = $date_input.data('max-view');
options.startView = $date_input.data('start-view');
if ($date_input.data('start-date')) options.startDate = $date_input.data('start-date');
if ($date_input.data('end-date')) options.endDate = $date_input.data('end-date');
$date_input.datetimepicker(options);
});
/* searchable select */
$base.find('select[data-autocomplete]').each(function(i, elem) {
var required = $(elem).data('required');
var options = {
language: {
errorLoading: function() { return WCS_I18N.s2_errorloading; },
noResults: function () { return WCS_I18N.s2_nomatches; },
inputTooShort: function (input, min) { return WCS_I18N.s2_tooshort; },
loadingMore: function () { return WCS_I18N.s2_loadmore; },
searching: function () { return WCS_I18N.s2_searching; }
}
};
options.placeholder = $(elem).find('[data-hint]').data('hint');
if (!required) {
if (!options.placeholder) options.placeholder = '...';
options.allowClear = true;
}
$(elem).select2(options);
});
/* searchable select using a data source */
$base.find('select[data-select2-url]').each(function(i, elem) {
var required = $(elem).data('required');
// create an additional hidden field to hold the label of the selected
// option, it is necessary as the server may not have any knowledge of
// possible options.
var $input_display_value = $('<input>', {
type: 'hidden',
name: $(elem).attr('name') + '_display',
value: $(elem).data('initial-display-value')
});
$input_display_value.insertAfter($(elem));
var options = {
minimumInputLength: 1,
formatResult: function(result) { return result.text; },
language: {
errorLoading: function() { return WCS_I18N.s2_errorloading; },
noResults: function () { return WCS_I18N.s2_nomatches; },
inputTooShort: function (input, min) { return WCS_I18N.s2_tooshort; },
loadingMore: function () { return WCS_I18N.s2_loadmore; },
searching: function () { return WCS_I18N.s2_searching; }
},
templateSelection: function(data, container) {
if (data.edit_related_url) {
$(data.element).attr('data-edit-related-url', data.edit_related_url);
}
return data.text;
}
};
if (!required) {
options.placeholder = '...';
options.allowClear = true;
}
var url = $(elem).data('select2-url');
if (url.indexOf('/api/autocomplete/') == 0) { // local proxying
var data_type = 'json';
} else {
var data_type = 'jsonp';
}
options.ajax = {
delay: 250,
dataType: data_type,
data: function(params) {
return {q: params.term, page_limit: 50};
},
processResults: function (data, params) {
return {results: data.data};
},
url: function() {
var url = $(elem).data('select2-url');
url = url.replace(/\[var_.+?\]/g, function(match, g1, g2) {
// compatibility: if there are [var_...] references in the URL
// replace them by looking for other select fields on the same
// page.
var related_select = $('#' + match.slice(1, -1));
var value_container_id = $(related_select).data('valuecontainerid');
return $('#' + value_container_id).val() || '';
});
return url;
}
};
var select2 = $(elem).select2(options);
$(elem).on('change', function() {
// update _display hidden field with selected text
var $selected = $(elem).find(':selected').first();
var text = $selected.text();
$input_display_value.val(text);
// update edit-related button href
$(elem).siblings('.edit-related').attr('href', '').hide();
if ($selected.attr('data-edit-related-url')) {
$(elem).siblings('.edit-related').attr('href', $selected.attr('data-edit-related-url') + '?_popup=1').show();
}
});
if ($input_display_value.val()) {
// if the _display hidden field was created with an initial value take it
// and create a matching <option> in the real <select> widget, and use it
// to set select2 initial state.
var option = $('<option></option>', {value: $(elem).data('value')});
option.appendTo($(elem));
option.text($input_display_value.val());
if ($(elem).data('initial-edit-related-url')) {
option.attr('data-edit-related-url', $(elem).data('initial-edit-related-url'));
}
select2.val($(elem).data('value')).trigger('change');
$(elem).select2('data', {id: $(elem).data('value'), text: $(elem).data('initial-display-value')});
}
});
/* Make table widgets responsive */
$base.find('.TableWidget, .SingleSelectTableWidget, .TableListRowsWidget').each(function (i, elem) {
const table = elem.querySelector('table');
new Responsive_table_widget(table);
});
/* Add class to reset error style on change */
$base.find('.widget-with-error').each(function(i, elem) {
$(elem).find('input, select, textarea').on('change', function() {
$(this).parents('.widget-with-error').addClass('widget-reset-error');
});
});
}
add_js_behaviours($('form[data-live-url], form[data-backoffice-preview]'));
// Form with error
const errornotice = document.querySelector('form:not([data-backoffice-preview]) .errornotice');
if (errornotice) {
document.body.classList.add('form-with-error');
errornotice.setAttribute('tabindex', '-1');
errornotice.focus();
}
$(window).bind('pageshow', function(event) {
$('form').removeClass('disabled-during-submit');
});
$('form button').on('click', function(event) {
if ($(this).hasClass('download')) {
$(this).parents('form').addClass('download-button-clicked');
} else {
$(this).parents('form').removeClass('download-button-clicked');
}
return true;
});
$('form .buttons.submit button').on('click', function (event) {
if (autosave_is_running) {
autosave_button_to_click_on_complete = event.target;
/* prevent more autosave */
autosave_timeout_id = null;
event.preventDefault();
}
});
$('form').on('submit', function(event) {
var $form = $(this);
/* prevent more autosave */
if (autosave_timeout_id !== null) {
window.clearTimeout(autosave_timeout_id);
autosave_timeout_id = null;
}
$form.addClass('disabled-during-submit');
if ($form.hasClass('download-button-clicked')) {
/* form cannot be disabled for download buttons as the user will stay on
* the same page; enable it back after a few seconds. */
setTimeout(function() { $form.removeClass('disabled-during-submit'); }, 3000);
}
if ($form[0].wait_for_changes) {
var waited = 0;
var $button = $(event.originalEvent.submitter);
if (! $button.is('button')) {
$button = $('form .buttons .submit-button button');
}
var wait_id = setInterval(function() {
waited += 1;
if (! $form[0].wait_for_changes) {
clearInterval(wait_id);
$button.click();
return;
} else if (waited > 5) {
$form[0].wait_for_changes = false;
}
}, 200);
return false;
}
return true;
});
var live_evaluation = null;
if ($('div[data-live-source]').length || $('.submit-user-selection').length) {
$('form[data-live-url]').on('wcs:change', function(ev, data) {
if (live_evaluation) {
live_evaluation.abort();
}
var new_data = $(this).serialize();
if (data && data.modified_field) {
new_data += '&modified_field_id=' + data.modified_field;
if (data.modified_block) new_data += '&modified_block_id=' + data.modified_block;
if (data.modified_block_row) new_data += '&modified_block_row=' + data.modified_block_row;
}
$('.widget-prefilled').each(function(idx, elem) {
new_data += '&prefilled_' + $(elem).data('field-id') + '=true';
});
var live_url = $(this).data('live-url');
live_evaluation = $.ajax({
type: 'POST',
url: live_url,
dataType: 'json',
data: new_data,
headers: {'accept': 'application/json'},
success: function(json) {
if (json.result === "error") {
console.log('error in /live request: ' + json.reason);
return;
}
$.each(json.result, function(key, value) {
if (value.block_id && value.block_row) {
var $widget = $('[data-field-id="' + value.block_id + '"] [data-block-row="' + value.block_row + '"] [data-field-id="' + value.field_id + '"]');
} else if (value.block_id) {
var $widget = $('[data-field-id="' + value.block_id + '"] [data-field-id="' + value.field_id + '"]');
} else {
var $widget = $('[data-field-id="' + key + '"]');
}
if (value.visible) {
var was_visible = $widget.is(':visible');
$widget.css('display', '');
if ($widget.hasClass('MapWidget') && !was_visible) {
$widget.find('.qommon-map').trigger('qommon:invalidate');
}
} else {
$widget.hide();
}
if (value.items && $widget.is('.RadiobuttonsWidget')) {
var current_value = $widget.find('input:checked').val();
var $hint = $widget.find('.hint').detach();
var input_name = $widget.data('widget-name');
var $content = $widget.find('.content');
var length_first_items = 0;
$content.empty();
for (var i=0; i<value.items.length; i++) {
var $label = $('<label></label>');
var $input = $('<input>', {type: 'radio', value: value.items[i].id, name: input_name});
if (value.items[i].id == current_value) {
$input.attr('checked', 'checked');
}
if (value.items[i].disabled) {
$input.prop('disabled', true);
$label.addClass('disabled');
}
var $span = $('<span></span>', {text: value.items[i].text});
$input.appendTo($label);
$span.appendTo($label);
$label.appendTo($content);
if (i < 3) {
length_first_items += value.items[i].text.length;
}
}
if (value.items.length <= 3 && length_first_items < 40) {
$widget.addClass('widget-inline-radio');
} else {
$widget.removeClass('widget-inline-radio');
}
$hint.appendTo($content);
} else if (value.items && $widget.is('.CheckboxesWidget')) {
var widget_name = $widget.data('widget-name');
var $ul = $widget.find('ul');
var current_value = $ul.find('input[type=checkbox]'
).filter(function() {return this.checked}
).map(function() {return this.name;}
).toArray();
var base_for_name = $ul.data('base-for-name');
var input_name = $widget.data('widget-name');
$ul.empty();
for (var i=0; i<value.items.length; i++) {
var $li = $('<li>');
var $label = $('<label>', {'for': base_for_name + i});
var $input = $('<input>', {
type: 'checkbox', 'id': base_for_name + i,
value: 'yes', name: widget_name + '$element' + value.items[i].id});
if (current_value.indexOf(widget_name + '$element' + value.items[i].id) != -1) {
$input.attr('checked', 'checked');
}
if (value.items[i].disabled) {
$input.prop('disabled', true);
$li.addClass('disabled');
}
var $span = $('<span>', {text: value.items[i].text});
$input.appendTo($label);
$span.appendTo($label);
$label.appendTo($li);
$li.appendTo($ul);
}
} else if (value.items) {
// replace <select> contents
var $select = $widget.find('select');
var current_value = $select.val();
var hint = $widget.find('option[data-hint]').data('hint');
$select.empty();
if (hint) {
var $option = $('<option></option>', {value: '', text: hint});
$option.attr('data-hint', hint);
$option.appendTo($select);
}
for (var i=0; i<value.items.length; i++) {
var $option = $('<option></option>', {value: value.items[i].id, text: value.items[i].text});
if ((Array.isArray(current_value) && current_value.indexOf(value.items[i].id.toString()) != -1) ||
value.items[i].id == current_value) {
$option.attr('selected', 'selected');
value.items[i].selected = true;
}
if (value.items[i].disabled) {
$option.prop('disabled', true);
}
$option.appendTo($select);
}
$select.trigger('wcs:options-change', {items: value.items});
}
if (typeof value.content !== 'undefined') {
$widget.each(function(idx, widget) {
if ($widget.hasClass('comment-field')) {
// replace comment content
$widget.html(value.content);
} else {
if ($(widget).is('.widget-prefilled') || $(widget).is('.widget-readonly') || data.modified_field == 'user') {
// replace text input value
const $text_inputs = $(widget).find('input[type=text], input[type=tel], input[type=numeric], input[type=email], input[type=date], textarea');
$text_inputs.val(value.content)
$text_inputs.each((_, el) => el.dispatchEvent(new Event('wcs:live-update')));
if ($(widget).is('.DateWidget')) {
// Set both hidden input for real value, and text input for
// formatted date. This will also set the old date picker
// to the formatted value, which is expected.
$(widget).find('input[type=hidden]').val(value.content);
$(widget).find('input[type=text]').val(value.text_content);
}
if ($widget.hasClass('CheckboxWidget')) {
// replace checkbox input value
$widget.find('input[type=checkbox]').prop('checked', value.content);
}
// replace select value
$(widget).find('select').val(value.content);
if ($.type(value.content) == 'string' && value.content.indexOf('"') == -1) {
// replace radio value
$(widget).find('input[type=radio]').prop('checked', false);
$(widget).find('input[type=radio][value="'+value.content+'"]').prop('checked', true);
}
if (data.modified_field == 'user' && value.locked) {
$(widget).addClass('widget-readonly');
$(widget).find('input').attr('readonly', 'readonly');
}
}
}
});
}
if (value.source_url) {
// json change of URL
$widget.find('[data-select2-url]').data('select2-url', value.source_url);
}
});
}
});
});
}
if ($('div[data-live-source]').length) {
$('form').on('change input paste wcs:change',
'div[data-live-source] input:not([type=file]), div[data-live-source] select, div[data-live-source] textarea',
function(ev) {
var params = {};
params.modified_field = $(this).closest('[data-field-id]').data('field-id');
if ($(this).parents('.BlockWidget').length) {
params.modified_block = $(this).closest('.BlockWidget').data('field-id');
params.modified_block_row = $(this).closest('.BlockSubWidget').data('block-row');
}
$(this).parents('form').trigger('wcs:change', params);
});
}
$('form div[data-live-source]').parents('form').trigger('wcs:change', {modified_field: 'init'});
$('div.widget-prefilled').on('change input paste', function(ev) {
$(this).removeClass('widget-prefilled');
});
$('div.widget-prefilled input[type=radio], div.widget-prefilled input[type=checkbox]').on('change', function(ev) {
$(this).closest('div.widget').removeClass('widget-prefilled');
});
function disable_single_block_remove_button() {
$('.BlockSubWidget button.remove-button').each(function(i, elem) {
if ($(this).parents('.BlockWidget').find('.BlockSubWidget').length == 1) {
$(this).prop('disabled', true);
}
});
}
if ($('.BlockWidget').length) {
disable_single_block_remove_button();
$('form').on('click', '.BlockSubWidget button.remove-button', function() {
if ($(this).parents('.BlockWidget').find('.BlockSubWidget').length > 1) {
const $add_button = $(this).parents('.BlockWidget').find('.list-add');
/* rename attributes in following blocks */
const $subwidget = $(this).parents('.BlockSubWidget').first();
const name_parts = $subwidget.data('widget-name').match(/(.*)(\d+)$/);
const prefix = name_parts[1];
const idx = parseInt(name_parts[2]);
function replace_prefix(elem, prefix1, prefix2) {
$.each(elem.attributes, function() {
if (this.specified) {
this.value = this.value.replace(prefix1, prefix2);
}
});
}
$subwidget.nextAll('.BlockSubWidget').each(function(i, elem) {
const prefix1 = prefix + (idx + i + 1);
const prefix2 = prefix + (idx + i);
replace_prefix(elem, prefix1, prefix2);
$(elem).find('*').each(function(i, elem_child) { replace_prefix(elem_child, prefix1, prefix2); });
});
/* then remove row */
$subwidget.remove();
disable_single_block_remove_button();
/* display button then give it focus */
$add_button.show().find('button').focus();
}
return false;
});
$('form').on('click', 'div.BlockWidget .list-add button', function(ev) {
ev.preventDefault();
const $block = $(this).parents('.BlockWidget');
const block_id = $block.data('field-id');
const $button = $(this);
const $form = $(this).parents('form');
var form_data = $form.serialize();
form_data += '&' + $button.attr('name') + '=' + $button.val();
$.ajax({
type: 'POST',
url: $form.attr('action') || window.location.pathname,
data: form_data,
headers: {'x-wcs-ajax-action': 'block-add-row'},
success: function(result, text_status, jqXHR) {
const $new_block = $(result).find('[data-field-id="' + block_id + '"]');
$block.replaceWith($new_block);
const $new_blockrow = $new_block.find('.BlockSubWidget').last();
add_js_behaviours($('[data-field-id="' + block_id + '"]'));
$('form').trigger('wcs:block-row-added');
$(document).trigger('wcs:maps-init');
if ($new_block.find('[data-live-source]')) {
$('form div[data-live-source]').parents('form').trigger('wcs:change', {modified_field: 'init'});
}
$new_blockrow[0].setAttribute('tabindex', '-1');
$new_blockrow[0].focus();
if ($new_blockrow.position().top < window.scrollY) {
$new_blockrow[0].scrollIntoView({behavior: "instant", block: "center", inline: "nearest"});
}
}
});
});
}
});
function check_condition(button, widget_id) {
var $form = $('form[data-check-condition-url]');
var form_content = $form.serialize();
$.ajax({
type: 'POST',
url: $form.attr('data-check-condition-url') + '?field=' + widget_id,
data: form_content,
success: function(json) {
if (json.err === 0) {
button.textContent = 'ok';
} else {
button.textContent = 'err: ' + json.msg;
}
},
});
}
/*
* Live Field Validation
*/
const LiveValidation = (function(){
const excludedField = function (field) {
if (field.disabled ) return true
const excludedType = [ 'button', 'reset', 'submit' ]
if (excludedType.includes(field.type)) return true
return false
}
/*
* Check validy of field by HTML attributs
* cf JS constaint validation API
* return first error fouded
*/
const hasAttrError = function (field) {
const validityState = field.validity
if (validityState.valid) return
let errorType
for (const key in validityState) {
if (validityState[key]) {
errorType = key
break
}
}
return errorType
}
/*
* Check Validy of field by request to server
*/
const hasServerError = function (field, form, url) {
return fetch( url+field.name, {
method: 'POST',
body: new FormData(form)
})
.then( (response) => response.json() )
.then( (json) => {
if (json.err === 0) {
return
} else {
let errorType
for (const key in json) {
if (json[key] === true) {
errorType = key
break
}
}
return errorType
}
})
}
class FieldLiveValidation {
constructor (widget, formDatas) {
this.widget = widget
this.name = widget.dataset.widgetName
this.errorClass = "widget-with-error"
this.errorEl = this.setErrorEl(formDatas.errorTpl.content.children[0])
this.checkUrl = formDatas.checkUrl
this.hasError = false
this.init()
}
setErrorEl = function(errorTpl) {
const errorEl = document.importNode(errorTpl)
errorEl.id = errorEl.id.replace('fieldname', this.name)
return errorEl
}
async toggleStatus(field) {
if (excludedField(field)) return
const attrError = hasAttrError(field);
const serverError = this.widget.dataset.supportsCheckCondition
? await hasServerError(field, field.form, this.checkUrl)
: false
const error = attrError ? attrError : serverError
if (error) {
this.showError(field, error)
} else {
this.removeError(field, error)
}
}
showError(field, error) {
if (this.hasError === error) return
this.widget.classList.add(this.errorClass)
const errorElMessage = document.getElementById(`error_${this.name}_${error}`).innerHTML
this.errorEl.innerHTML = errorElMessage
this.widget.appendChild(this.errorEl)
field.setAttribute("aria-invalid", "true")
field.setAttribute("aria-describedby", this.errorEl.id)
this.hasError = error
}
removeError(field, error) {
if (error) return
this.errorEl.remove()
field.setAttribute("aria-invalid", "false")
field.setAttribute("aria-describedby", this.errorEl.id)
this.widget.classList.remove(this.errorClass)
this.hasError = false
}
init() {
// Check if field is allready on error
if (this.widget.classList.contains(this.errorClass)) {
this.hasError = true;
// Check if error element exist allready
const existingErrorEl = document.getElementById(this.errorEl.id)
if (existingErrorEl)
this.errorEl = existingErrorEl;
}
// Events
this.widget.addEventListener('blur', (event) => {
this.toggleStatus(event.target)
}, true);
// If field has Error, check when it change with debounce
let timeout;
this.widget.addEventListener('input', (event) => {
if (this.hasError) {
clearTimeout(timeout)
timeout = setTimeout(() => {
this.toggleStatus(event.target)
}, 500)
}
}, true);
}
}
return FieldLiveValidation
})()
document.addEventListener('DOMContentLoaded', function(){
const form = document.querySelector('form[data-check-condition-url]')
const formWidgets = form.querySelectorAll('.widget')
let formDatas = {
errorTpl: document.getElementById('form_error_tpl'),
checkUrl: form.dataset.checkConditionUrl + '?field=',
}
formWidgets.forEach((widget) => {
new LiveValidation(widget, formDatas)
})
})