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

/**
 * This file provides a set of global handlers.
 */

var proxy_elements_selector = ':checkbox[data-proxyfor], :radio[data-proxyfor]';
var proxied_elements_selector = ':checkbox[data-proxiedby], :radio[data-proxiedby]';

function connectProxyAndProxied() {
    $(proxy_elements_selector).each(function () {
        const proxy = $(this);
        const proxyId = proxy.uniqueId().attr('id');
        const proxied = proxy.data('proxyfor');
        // The following seems like a hack but works perfectly fine.
        $(proxied).each(function () {
            const proxiedBy = ($(this).attr('data-proxiedby') || '').split(',').filter(a => a.length > 0);
            if (!proxiedBy.includes(`#${proxyId}`)) {
                proxiedBy.push(`#${proxyId}`);
            }

            $(this)
                .attr('data-proxiedby', proxiedBy.join(','))
                .data('proxiedby', `#${proxyId}`);
        });
    }).trigger('update.proxy');
}

// Use a checkbox as a proxy for a set of other checkboxes. Define
// proxied elements by a css selector in attribute "data-proxyfor".
$(document).on('change', proxy_elements_selector, function (event, force) {
    // Detect if event was triggered natively (triggered events have no
    // originalEvent)
    if (event.originalEvent !== undefined || !!force) {
        const proxied = $(this).data('proxyfor');
        $(proxied)
            .filter(':not(:disabled)')
            .prop('checked', this.checked)
            .prop('indeterminate', false)
            .each((index, element) => {
                const proxiedBy = $(element).attr('data-proxiedby');
                $(proxiedBy)
                    .filter((idx, item) => item !== this)
                    .trigger('update.proxy');

                const event = new Event('change');
                element.dispatchEvent(event);
                if (element.matches('[data-proxyfor],[data-activates],[data-deactivates],[data-hides],[data-shows]')) {
                    $(element).trigger('change', [true]);
                }
    }
}).on('update.proxy', proxy_elements_selector, function () {
    const proxied = $(this).data('proxyfor');
    const $proxied = $(proxied).filter(':not(:disabled)');
    const $checked = $proxied.filter(':checked');
    const $indeterminate = $proxied.filter(function () {
        return $(this).prop('indeterminate');
    });
    $(this).prop('checked', $proxied.length > 0 && $proxied.length === $checked.length);
    $(this).prop(
        'indeterminate',
        ($checked.length > 0 && $checked.length < $proxied.length) || $indeterminate.length > 0
    );
    $(this).trigger('change');
}).on('change', proxied_elements_selector, function () {
    //In case of radio buttons in a group that are deselected,
    //we must trigger the update.proxy event for each radio
    //button in the group, if the proxy is another element
    //than the proxy for "this" element.
    if ($(this).is(':radio')) {
        const proxy = $(this).data('proxiedby');
        const name = $(this).attr('name');
        const radio_button_group = $(`:radio[name="${name}"]`);
        $(radio_button_group).each(function () {
            const button_proxy = $(this).data('proxiedby');
            if (button_proxy !== proxy) {
                $(button_proxy).trigger('update.proxy');
            }
        });
    } else {
        const proxy = $(this).attr('data-proxiedby');
        $(proxy).trigger('update.proxy');
    }
});

STUDIP.ready(connectProxyAndProxied);
$(document).on('refresh-handlers', connectProxyAndProxied);

// Use a checkbox or radiobox as a toggle switch for the disabled attribute of
// another set of elements. Define set of elements to disable/enable if item is
// neither :checked nor :indeterminate by a css selector in attribute
// "data-activates" / "deactivates".
$(document).on('change', '[data-activates],[data-deactivates]', function() {
    if (!$(this).is(':checkbox,:radio')) {
        return;
    }

    ['activates', 'deactivates'].forEach((type) => {
        var selector = $(this).data(type);
        if (selector === undefined || $(this).prop('disabled')) {
            return;
        }

        var state = $(this).prop('checked') || $(this).prop('indeterminate') || false;
        $(selector).each(function() {
            var condition = $(this).data(`${type}Condition`),
                toggle = state && (!condition || $(condition).length > 0);
            $(this)
                .attr('disabled', type === 'activates' ? !toggle : toggle)
                .trigger('update.proxy');
        });
    });
});

STUDIP.ready((event) => {
    $('[data-activates],[data-deactivates]', event.target).trigger('change');
});

// Use a select as a toggle switch for the disabled attribute of another
// element. Define element to disable if select has a value different from
// an empty string by a css selector in attribute "data-activates".
$(document).on('change update.proxy', 'select[data-activates]', function() {
    var activates = $(this).data('activates'),
        disabled = $(this).is(':disabled') || $(this).val().length === 0;
    $(activates).attr('disabled', disabled);
});

STUDIP.ready((event) => {
    $('select[data-activates]', event.target).trigger('change');
});

//
$(document).on('change', '[data-hides],[data-shows]', function () {
    if (!$(this).is(':checkbox,:radio')) {
        return;
    }

    ['hides', 'shows'].forEach((type) => {
        var selector = $(this).data(type);
        if (selector === undefined || $(this).prop('disabled')) {
            return;
        }

        var state = $(this).prop('checked') || $(this).prop('indeterminate') || false;
        $(selector).each(function() {
            var condition = $(this).data(`${type}Condition`),
                toggle = state && (!condition || $(condition).length > 0);
            $(this)
                .toggle(type === 'shows' ? toggle : !toggle)
                .trigger('update.proxy');
        });
    });
});
STUDIP.ready(event => {
    $('[data-hides],[data-shows]', event.target).trigger('change');
});

// Enable the user to set the checked state on a subset of related
// checkboxes by clicking the first checkbox of the subset and then
// clicking the last checkbox of the subset while holding down the shift
// key, thus toggling all the checkboxes in between.
// This only works if the first and last checkbox of the subset are set
// to the same state.
var last_element = null;
$(document).on('click', '[data-shiftcheck] :checkbox', function(event) {
    if (!event.originalEvent || last_element === event.target) {
        return;
    }

    if (last_element !== null && event.shiftKey) {
        var $this = $(event.target),
            $form = $this.closest('form'),
            name = $this.attr('name'),
            state = $this.prop('checked'),
            $last = $(last_element),
            children,
            idx0,
            idx1,
            tmp;

        if ($form.is($last.closest('form')) && name === $last.attr('name') && state === $last.prop('checked')) {
            children = $form.find(':checkbox[name="' + name + '"]:not(:disabled)');
            idx0 = children.index(event.target);
            idx1 = children.index(last_element);
            if (idx0 > idx1) {
                tmp = idx0;
                idx0 = idx1;
                idx1 = tmp;
            }
            children.slice(idx0, idx1).prop('checked', state);
        }
    }

    last_element = event.target;
});

// Lets the user confirm a specific action (submit or click event).
function confirmation_handler(event) {
    if (!event.isDefaultPrevented()) {
        event.stopPropagation();
        event.preventDefault();

        var element = $(event.currentTarget).closest('[data-confirm]'),
            question =
                element.data().confirm ||
                element.attr('title') ||
                element.find('[title]:first').attr('title') ||
                $gettext('Wollen Sie die Aktion wirklich ausführen?');

        STUDIP.Dialog.confirm(question).done(function() {
            var content = element.data().confirm;

            // We need to trigger the native event because for
            // some reason, jQuery's .trigger() won't always
            // work. Thus the data-confirm attribute will be removed
            // so that the original event can be executed
            element
                .removeAttr('data-confirm')
                .get(0)[event.type]();

            // Reapply the data-confirm attribute
            window.setTimeout(function() {
                element.attr('data-confirm', content);
            }, 0);
        });
    }
}
$(document).on(
    'click',
    'a[data-confirm],input[data-confirm],button[data-confirm],img[data-confirm]',
    confirmation_handler
);
$(document).on('submit', 'form[data-confirm]', confirmation_handler);

// Ensures an element has the same value as another element.
$(document).on('change', 'input[data-must-equal]', function() {
    var value = $(this).val(),
        rel = $(this).data().mustEqual,
        other = $(rel).val(),
        labels = $.map([this, rel], function(element) {
            var label = $(element)
                .closest('label')
                .text();
            label = label || $('label[for="' + $(element).attr('id') + '"]').text();
            return $.trim(label.split(':')[0]);
        }),
        error_message = $gettext('Die beiden Werte "$1" und "$2" stimmen nicht überein. '),
        matches = error_message.match(/\$\d/g);

    $.each(matches, function(i) {
        error_message = error_message.replace(this, labels[i]);
    });

    if (value !== other) {
        this.setCustomValidity(error_message);
    } else {
        this.setCustomValidity('');
    }
});

//Generalisation: The enter-accessible class allows an element to be accessible via keyboard
//by triggering the click event when the enter key is pressed.
$(document).on('keydown', '.enter-accessible', function(event) {
    if (event.code === 'Enter') {
        //The enter key has been pressed.
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed

$(document).on('click keydown', '[data-toggles]', function (event) {
    if ((event.type === 'keydown' && event.key === 'Enter') || event.type === 'click') {
        const target = event.currentTarget.dataset.toggles;
        //Check if the target is a checkbox. These have to be toggled differently than
        //other elements:
        if ($(target).is(':checkbox')) {
            //Toggle the checked state of the checkbox:
            $(target).prop('checked', !$(target).prop('checked'));
        } else {
            //Do the normal toggle operation:
            $(target).toggle();
        }
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed

        const controls = $(event.currentTarget).attr('aria-controls');
        if (controls) {
            // Find elements which control the expanded status of the same element.
            const elements = $('[aria-controls="' + controls + '"]');
            const expanded = $(event.currentTarget).attr('aria-expanded') === 'true';
            // Set the aria-expanded status accordingly.
            elements.attr('aria-expanded', !expanded);
        }

        event.preventDefault();
    }
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
});