Skip to content
Snippets Groups Projects
forms.js 10.9 KiB
Newer Older
import { $gettext } from '../lib/gettext.js';

// Allow fieldsets to collapse
$(document).on(
    'click',
    'form.default fieldset.collapsable legend,form.default.collapsable fieldset legend',
    function() {
        $(this)
            .closest('fieldset')
            .toggleClass('collapsed');
    }
);

// Display a visible hint that indicates how many characters the user may
// input if the element has a maxlength restriction.

$(document).on('focus', 'form.default [maxlength]:not(.no-hint)', function() {
    if (!$(this).is('textarea,input') || $(this).data('length-hint') || $(this).is('[readonly],[disabled]')) {
        return;
    }

    var width = $(this).outerWidth(true),
        hint = $('<div class="length-hint">').hide(),
        wrap = $('<div class="length-hint-wrapper">').width(width),
        timeout = null;

    $(this).wrap(wrap);

    hint.text($gettext('Zeichen verbleibend: '));

    hint.append('<span class="length-hint-counter">');
    hint.insertBefore(this);

    $(this)
        .focus(function() {
            clearTimeout(timeout);
            timeout = setTimeout(function() {
                hint.finish().show('slide', { direction: 'down' }, 300);
            }, 200);
        })
        .blur(function() {
            clearTimeout(timeout);
            timeout = setTimeout(function() {
                hint.finish().hide('slide', { direction: 'down' }, 300);
            }, 200);
        })
        .on('focus propertychange change keyup', function() {
            var count = $(this).val().length,
                max = parseInt($(this).attr('maxlength'), 10);

            hint.find('.length-hint-counter').text(max - count);
        });

    $(this).data('length-hint', true);

    setTimeout(
        function() {
            $(this).focus();
        }.bind(this),
        0
    );
});

// Automatic form submission handler when a select has changed it's value.
// Due to accessibility issues, an intuitive select[onchange=form.submit()]
// leads to terrible behaviour when invoked not by mouse. The form is
// submitted upon _every_ change, including key strokes.
// Thus, we need to overwrite this behaviour. Breakdown of this solution:
//
// - Only submit when the value has actually changed
// - Always submit when pressing enter (keycode 13)
// - Always check for change on blur event
//
// - Store whether the element was activated by click event
// - If so, submit upon next change event
// - Otherwise submit when enter has been pressed
//
// Be aware: All select[onchange*="submit()"] will be rewritten to
// select.submit-upon-select and have the onchange attribute removed.
// This might lead to unexpected behaviour.

// Ensure, every .submit-upon-select has an defaultSelected option.
$(document)
    .on('focus', 'select[onchange*="submit()"]', function() {
        $(this)
            .removeAttr('onchange')
            .addClass('submit-upon-select');
    })
    .on('click mousedown', 'select.submit-upon-select', function(event) {
        // Firefox and Chrome handle click events on selects differently,
        // thus we need the mousedown event and the click event is needed for
        // select2 elements. Please do not change!

        $(this).data('wasClicked', true);
    })
    .on('change', 'select.submit-upon-select', function(event) {
        // Trigger blur event if element was clicked in the beginning

        if ($(this).data('wasClicked')) {
            $(this).trigger('blur');
        }
    })
    .on('focusout keyup keypress keydown select', 'select.submit-upon-select', function(event) {
        var shouldSubmit = event.type === 'keyup' ? event.which === 13 : $(this).data('wasClicked'),
            is_default = $('option:selected', this).prop('defaultSelected');

        // Submit only if value has changed and either enter was pressed or
        // select was opened by click
        if (!is_default && shouldSubmit) {
            $(this)
                .closest('form')
                .submit();
            return false;
        }
    });

STUDIP.ready((event) => {
    $('.submit-upon-select', event.target).each(function() {
        var has_default_selected =
            $('option', this).filter(function() {
                return this.defaultSelected;
            }).length > 0;
        if (!has_default_selected) {
            $('option', this)
                .first()
                .prop('defaultSelected', true);
        }
    });
});


// simulate formaction attribute for input[type=image] in IE11
$(document).on('click', 'input[type=image][formaction]', function() {
    if ($(this).attr('data-confirm') === undefined) {
        $(this)
            .closest('form')
            .attr('action', $(this).attr('formaction'));
    }
});

// Use select2 for crossbrowser compliant select styling and
// handling
$.fn.select2.amd.define('select2/i18n/de', [], function() {
    return {
        inputTooLong: function(e) {
            var t = e.input.length - e.maximum;
            return $gettext('Bitte %u Zeichen weniger eingeben').replace('%u', t);
        },
        inputTooShort: function(e) {
            var t = e.minimum - e.input.length;
            return $gettext('Bitte %u Zeichen mehr eingeben').replace('%u', t);
        },
        loadingMore: function() {
            return $gettext('Lade mehr Ergebnisse...');
        },
        maximumSelected: function(e) {
            var t = [
                $gettext('Sie können nur %u Eintrag auswählen'),
                $gettext('Sie können nur %u Einträge auswählen')
            ];
            return t[e.maximum === 1 ? 0 : 1].replace('%u', e.maximum);
        },
        noResults: function() {
            return $gettext('Keine Übereinstimmungen gefunden');
        },
        searching: function() {
            return $gettext('Suche...');
        }
    };
});
$.fn.select2.defaults.set('language', 'de');

function createSelect2(element) {
    if ($(element).data('select2')) {
        return;
    }

    var select_classes = $(element)
            .removeClass('select2-awaiting')
            .attr('class'),
        option = $('<option>'),
        width = $(element).outerWidth(true),
        cloned = $(element)
            .clone()
            .css('opacity', 0)
            .appendTo('body'),
        wrapper = $('<div class="select2-wrapper">').css('display', cloned.css('display')),
        placeholder;

    cloned.remove();
    $(wrapper)
        .add(element)
        .css('width', width);

    if ($('.is-placeholder', element).length > 0) {
        placeholder = $('.is-placeholder', element)
            .text()
            .trim();

        option.attr('selected', $(element).val() === '');
        $('.is-placeholder', element).replaceWith(option);
    }

    $(element).select2({
        adaptDropdownCssClass: function() {
            return select_classes;
        },
        allowClear: placeholder !== undefined,
        minimumResultsForSearch: $(element).closest('.sidebar').length > 0 ? 15 : 10,
        placeholder: placeholder,
        dropdownParent: $(element).closest('.ui-dialog,.sidebar,body'),
        templateResult: function(data, container) {
            if (data.element) {
                var option_classes = $(data.element).attr('class'),
                    element_data = $(data.element).data();
                $(container).addClass(option_classes);

                // Allow text color changes (calendar needs this)
                if (element_data.textColor) {
                    $(container).css('color', element_data.textColor);
                }
            }
            return data.text;
        },
        templateSelection: function(data, container) {
            var result = $('<span class="select2-selection__content">').text(data.text),
                element_data = $(data.element).data();
            if (element_data && element_data.textColor) {
                result.css('color', element_data.textColor);
            }

            if (element_data && element_data.colorClass) {
                result.addClass(element_data.colorClass);
            }
            return result;
        },
        width: 'style'
    });

    $(element)
        .next()
        .addBack()
        .wrapAll(wrapper);
}

STUDIP.ready(function () {
    // Well, this is really nasty: Select2 can't determine the select
    // element's width if it is hidden (by itself or by it's parent).
    // This is due to the fact that elements are not rendered when hidden
    // (which seems pretty obvious when you think about it) but elements
    // only have a width when they are rendered (pretty obvious as well).
    //
    // Thus, we need to handle the visible elements first and apply
    // select2 directly.
    $('select.nested-select:not(:has(optgroup)):visible').each(function() {
        createSelect2(this);
    });

    // The hidden need a little more love. The only, almost sane-ish
    // solution seems to be to attach a mutation observer to the closest
    // visible element from the requested select element and observe style,
    // class and attribute changes in order to detect when the select
    // element itself will become visible. Pretty straight forward, huh?
    $('select.nested-select:not(:has(optgroup)):hidden:not(.select2-awaiting)').each(function() {
        var observer = new window.MutationObserver(onDomChange);
        observer.observe($(this).closest(':visible')[0], {
            attributeOldValue: true,
            attributes: true,
            attributeFilter: ['style', 'class'],
            characterData: false,
            childList: true,
            subtree: false
        });

        $(this).addClass('select2-awaiting');
    });

    function onDomChange(mutations, observer) {
        mutations.forEach(function(mutation) {
            if ($('select.select2-awaiting', mutation.target).length > 0) {
                $('select.select2-awaiting', mutation.target)
                    .removeClass('select2-awaiting')
                    .each(function() {
                        createSelect2(this);
                    });
                observer.disconnect();
            }
        });
    }

    // Unfortunately, this code needs to be duplicated because jQuery
    // namespacing kind of sucks. If the below change handler is namespaced
    // and we trigger that namespaced event here, still all change handlers
    // will execute (which is bad due to $(select).change(form.submit())).
    $('select:not([multiple])').each(function() {
        $(this)
            .toggleClass('has-no-value', this.value === '')
            .blur();
    });
});

$(document)
    .on('change', 'select:not([multiple])', function() {
        $(this).toggleClass('has-no-value', this.value === '');
    })
    .on('dialog-close', function(event, data) {
        $('select.nested-select:not(:has(optgroup))', data.dialog).each(function() {
            if (!$(this).data('select2')) {
                return;
            }
            $(this).select2('close');
        });
    })
    .on('select2:open', 'select', function() {
        $(this).click();
    });