/* global gettext, Bloodhound */ function formatPrice (price, currency, locale) { if (!window.Intl || !Intl.NumberFormat) return price let priceToFormat = price if (currency === undefined) { currency = $('[data-currency]').data('currency') } if (locale === undefined) { locale = $('[data-locale]').data('locale') || $('[data-pretixlocale]').data('pretixlocale') } let opt = currency ? { style: 'currency', currency: currency } : null let nf = new Intl.NumberFormat(locale, opt) if (isNaN(priceToFormat) && priceToFormat.replaceAll) { // price is not a number, try to reformat based on locale/currency-format let replacements = { group: '', decimal: '.' } // format a dummy number to get parts of formatting and // replace group and decimal according to replacements // to hopefully get a parsable number nf.formatToParts(1234.567).forEach(function (part) { if (replacements.hasOwnProperty(part.type)) { priceToFormat = priceToFormat.replaceAll(part.value, replacements[part.type]) } }) if (isNaN(priceToFormat)) return price } try { return nf.format(priceToFormat) } catch (error) { return price } } let apiGET = function (url, callback) { $.getJSON(url, function (data) { callback(data) }) } let i18nToString = function (i18nstring) { let locale = $('body').attr('data-pretixlocale') if (i18nstring[locale]) { return i18nstring[locale] } else if (i18nstring['en']) { return i18nstring['en'] } for (const key in i18nstring) { if (i18nstring[key]) { return i18nstring[key] } } } $(document).ajaxError(function (event, jqXHR, settings, thrownError) { let c = $(jqXHR.responseText).filter('.container') if (jqXHR.status === 401 && jqXHR.getResponseHeader('X-Login-Url')) { window.location = jqXHR.getResponseHeader('X-Login-Url') + '?next=' + encodeURIComponent(location.pathname + location.search + location.hash) } else if (c.length > 0) { waitingDialog.hide() ajaxErrDialog.show(c.first().html()) } else if (thrownError !== 'abort' && thrownError !== '') { waitingDialog.hide() console.error(event, jqXHR, settings, thrownError) alert(gettext('Unknown error.')) } }) let form_handlers = function (el) { el.trigger('rescan.areYouSure') el.find('[data-formset]').formset( { animateForms: true, reorderMode: 'animate' } ) el.find('[data-formset]').on('formAdded', 'div', function (event) { form_handlers($(event.target)) }) el.find('[data-formset] [data-formset-sort]').on('click', function (event) { // Sort forms alphabetically by their first field let $formset = $(this).closest('[data-formset]') let $forms = $formset.find('[data-formset-form]').not('[data-formset-form-deleted]') let compareForms = function (form_a, form_b) { let a = $(form_a).find('input:not([name*=-ORDER]):not([name*=-DELETE]):not([name*=-id])').val() let b = $(form_b).find('input:not([name*=-ORDER]):not([name*=-DELETE]):not([name*=-id])').val() return a.localeCompare(b) } $forms = $forms.sort(compareForms) $forms.each(function (i, form) { let $order = $(form).find('[name*=-ORDER]') $order.val(i + 1) }) // Trigger visual reorder $formset.find('[name*=-ORDER]').first().trigger('change') }) // Vouchers el.find('#voucher-bulk-codes-generate').click(function () { if (!$('#voucher-bulk-codes-num').get(0).reportValidity()) return let num = $('#voucher-bulk-codes-num').val() let prefix = $('#voucher-bulk-codes-prefix').val() if (num != '') { let url = $(this).attr('data-rng-url') $('#id_codes').html('Generating...') $('.form-group:has(#voucher-bulk-codes-num)').removeClass('has-error') $.getJSON(url + '?num=' + num + '&prefix=' + escape(prefix), function (data) { $('#id_codes').val(data.codes.join('\n')) }) } else { $('.form-group:has(#voucher-bulk-codes-num)').addClass('has-error') $('#voucher-bulk-codes-num').focus() setTimeout(function () { $('.form-group:has(#voucher-bulk-codes-num)').removeClass('has-error') }, 3000) } }) el.find('.datetimepicker').each(function () { $(this).datetimepicker({ format: $(this).attr('data-format') ? $(this).attr('data-format') : $('body').attr('data-datetimeformat'), locale: $('body').attr('data-datetimelocale'), useCurrent: false, showClear: !$(this).prop('required'), icons: { time: 'fa fa-clock-o', date: 'fa fa-calendar', up: 'fa fa-chevron-up', down: 'fa fa-chevron-down', previous: $('html').hasClass('rtl') ? 'fa fa-chevron-right' : 'fa fa-chevron-left', next: $('html').hasClass('rtl') ? 'fa fa-chevron-left' : 'fa fa-chevron-right', today: 'fa fa-screenshot', clear: 'fa fa-trash', close: 'fa fa-remove' } }) if (!$(this).val()) { $(this).data('DateTimePicker').viewDate(moment().hour(0).minute(0).second(0)) } }) el.find('.datepickerfield').each(function () { let opts = { format: $(this).attr('data-format') ? $(this).attr('data-format') : $('body').attr('data-dateformat'), locale: $('body').attr('data-datetimelocale'), useCurrent: false, showClear: !$(this).prop('required'), icons: { time: 'fa fa-clock-o', date: 'fa fa-calendar', up: 'fa fa-chevron-up', down: 'fa fa-chevron-down', previous: $('html').hasClass('rtl') ? 'fa fa-chevron-right' : 'fa fa-chevron-left', next: $('html').hasClass('rtl') ? 'fa fa-chevron-left' : 'fa fa-chevron-right', today: 'fa fa-screenshot', clear: 'fa fa-trash', close: 'fa fa-remove' }, } if ($(this).is('[data-min]')) { opts['minDate'] = $(this).attr('data-min') opts['viewDate'] = $(this).attr('data-min') } if ($(this).is('[data-max]')) { opts['maxDate'] = $(this).attr('data-max') opts['viewDate'] = (opts.minDate // if minDate and maxDate are set, use the one closer to now as viewDate && Math.abs(+new Date(opts.minDate) - new Date()) < Math.abs(+new Date(opts.maxDate) - new Date()) ) ? opts.minDate : opts.maxDate } if ($(this).is('[data-is-payment-date]')) opts['daysOfWeekDisabled'] = JSON.parse($('body').attr('data-payment-weekdays-disabled')) $(this).datetimepicker(opts).on('dp.hide', function () { // when min/max is used in datetimepicker, closing and re-opening the picker opens at the wrong date // therefore keep the current viewDate and re-set it after datetimepicker is done hiding let $dtp = $(this).data('DateTimePicker') let currentViewDate = $dtp.viewDate() window.setTimeout(function () { $dtp.viewDate(currentViewDate) }, 50) }) if ($(this).parent().is('.splitdatetimerow')) { $(this).on('dp.change', function (ev) { let $timepicker = $(this).closest('.splitdatetimerow').find('.timepickerfield') let date = $(this).data('DateTimePicker').date() if (date === null) { return } if ($timepicker.val() === '') { if (/_(until|end|to)(_|$)/.test($(this).attr('name'))) { date.set({ hour: 23, minute: 59, second: 59 }) } else { date.set({ hour: 0, minute: 0, second: 0 }) } $timepicker.data('DateTimePicker').date(date) } }) } }) el.find('.timepickerfield').each(function () { let opts = { format: $(this).attr('data-format') ? $(this).attr('data-format') : $('body').attr('data-timeformat'), locale: $('body').attr('data-datetimelocale'), useCurrent: false, showClear: !$(this).prop('required'), icons: { time: 'fa fa-clock-o', date: 'fa fa-calendar', up: 'fa fa-chevron-up', down: 'fa fa-chevron-down', previous: $('html').hasClass('rtl') ? 'fa fa-chevron-right' : 'fa fa-chevron-left', next: $('html').hasClass('rtl') ? 'fa fa-chevron-left' : 'fa fa-chevron-right', today: 'fa fa-screenshot', clear: 'fa fa-trash', close: 'fa fa-remove' } } if ($(this).is('[data-is-payment-date]')) opts['daysOfWeekDisabled'] = JSON.parse($('body').attr('data-payment-weekdays-disabled')) $(this).datetimepicker(opts) }) el.find('.datetimepicker[data-date-after], .datepickerfield[data-date-after]').each(function () { let later_field = $(this), earlier_field = $($(this).attr('data-date-after')), update = function () { let earlier = earlier_field.data('DateTimePicker').date(), later = later_field.data('DateTimePicker').date() if (earlier === null) { earlier = false } else if (later !== null && later.isBefore(earlier) && !later.isSame(earlier)) { later_field.data('DateTimePicker').date(earlier.add(1, 'h')) } later_field.data('DateTimePicker').minDate(earlier) } update() earlier_field.on('dp.change', update) }) el.find('.datetimepicker[data-date-default], .datepickerfield[data-date-default]').each(function () { let fill_field = $(this), default_field = $($(this).attr('data-date-default')), show = function () { let fill_date = fill_field.data('DateTimePicker').date(), default_date = default_field.data('DateTimePicker').date() if (fill_date === null) { fill_field.data('DateTimePicker').defaultDate(default_date) } } fill_field.on('dp.show', show) }) function luminance (r, g, b) { // Algorithm defined as https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef let a = [r, g, b].map(function (v) { v /= 255 return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4) }) return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722 } function contrast (rgb1, rgb2) { // Algorithm defined at https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests let l1 = luminance(rgb1[0], rgb1[1], rgb1[2]) + 0.05, l2 = luminance(rgb2[0], rgb2[1], rgb2[2]) + 0.05, ratio = l1 / l2 if (l2 > l1) { ratio = 1 / ratio } return ratio.toFixed(1) } el.find('.colorpickerfield').colorpicker({ format: 'hex', align: 'left', customClass: 'colorpicker-2x', sliders: { saturation: { maxLeft: 200, maxTop: 200 }, hue: { maxTop: 200 }, alpha: { maxTop: 200 } } }).not('.no-contrast').on('changeColor create', function (e) { if (e.type == 'changeColor' && !e.value) { return } let rgb = $(this).colorpicker('color').toRGB() let c = contrast([255, 255, 255], [rgb.r, rgb.g, rgb.b]) let mark = 'times' let $icon = $(this).parent().find('.contrast-icon') if ($icon.length === 0 && $(this).parent().find('.contrast-state').length === 0) { $(this).parent().append('
') } let $note = $(this).parent().find('.contrast-state') if ($(this).val() === '') { $note.remove() } let icon, text, cls if (c > 7) { icon = 'fa-check-circle' text = gettext('Your color has great contrast and will provide excellent accessibility.') cls = 'text-success' } else if (c > 4.5) { icon = 'fa-info-circle' text = gettext('Your color has decent contrast and is sufficient for minimum accessibility requirements.') cls = '' } else { icon = 'fa-warning' text = gettext('Your color has insufficient contrast to white. Accessibility of your site will be impacted.') cls = 'text-danger' } if ($icon.length === 0) { $note.html('') .append(text) $note.removeClass('text-success').removeClass('text-danger').addClass(cls) } else { $icon.html('') $icon.attr('title', text) $icon.tooltip('destroy') window.setTimeout(function () { $icon.tooltip({ title: text }) }, 250) } }) function findDependency (searchString, sourceElement) { if (searchString.substr(0, 1) === '<') { return $(sourceElement).closest('form, .form-horizontal').find(searchString.substr(1)) } else { return $(searchString) } } el.find('input[data-checkbox-dependency]').each(function () { let initially_disabled = $(this).prop('disabled') let dependent = $(this), dependency = findDependency($(this).attr('data-checkbox-dependency'), this), update = function () { let enabled = dependency.prop('checked') dependent.prop('disabled', !enabled || initially_disabled).closest('.form-group, .form-field-boundary').toggleClass('disabled', !enabled) if (!enabled && !dependent.is('[data-checkbox-dependency-visual]')) { dependent.prop('checked', false) dependent.trigger('change') } } update() dependency.on('change', update) }) el.find('select[data-inverse-dependency], input[data-inverse-dependency]').each(function () { let dependent = $(this), dependency = findDependency($(this).attr('data-inverse-dependency'), this), update = function () { let enabled = !dependency.prop('checked') dependent.prop('disabled', !enabled).closest('.form-group, .form-field-boundary').toggleClass('disabled', !enabled) } update() dependency.on('change', update) }) el.find('div[data-display-dependency], textarea[data-display-dependency], input[data-display-dependency], select[data-display-dependency], button[data-display-dependency]').each(function () { let initially_disabled = $(this).prop('disabled') let dependent = $(this), dependency = findDependency($(this).attr('data-display-dependency'), this), update = function (ev) { let enabled = dependency.toArray().some(function (d) { if (d.disabled && !initially_disabled) return false if (d.type === 'checkbox' || d.type === 'radio') { return d.checked } else if (d.type === 'select-one') { let checkValue if ((checkValue = /^\/(.*)\/$/.exec(dependent.attr('data-display-dependency-regex')))) { return new RegExp(checkValue[1]).test(d.value) } else if ((checkValue = dependent.attr('data-display-dependency-value'))) { return d.value === checkValue } else { return !!d.value } } else { return (!!d.value && !d.value.match(/^0\.?0*$/g)) } }) if (dependent.is('[data-inverse]')) { enabled = !enabled } let $toggling = dependent if (dependent.is('[data-disable-dependent]')) { $toggling.attr('disabled', !enabled || initially_disabled).trigger('change') } const tagName = dependent.get(0).tagName.toLowerCase() if (tagName !== 'div' && tagName !== 'button') { $toggling = dependent.closest('.form-group') } if ($toggling.find(".has-error").length > 0) { enabled = true // Don't hide error message that makes form unsubmittable } if (ev) { if (enabled) { $toggling.stop().slideDown() } else { $toggling.stop().slideUp() } } else { $toggling.stop().toggle(enabled) } } update() dependency.each(function () { $(this).closest('.form-group').find('[name=' + $(this).attr('name') + ']').on('change dp.change', update) }) }) el.find('input[data-required-if], select[data-required-if], textarea[data-required-if]').each(function () { let dependent = $(this), dependencies = $($(this).attr('data-required-if')), update = function (ev) { let enabled = true dependencies.each(function () { let dependency = $(this) let e = (dependency.attr('type') === 'checkbox' || dependency.attr('type') === 'radio') ? dependency.prop('checked') : !!dependency.val() enabled = enabled && e }) dependent.prop('required', enabled).closest('.form-group').toggleClass('required', enabled).find('.optional').stop().animate({ opacity: enabled ? 0 : 1 }, ev ? 500 : 1) } update() dependencies.each(function () { let dependency = $(this) dependency.closest('.form-group').find('input[name=' + dependency.attr('name') + ']').on('change', update) dependency.closest('.form-group').find('input[name=' + dependency.attr('name') + ']').on('dp.change', update) }) }) el.find('div.scrolling-choice:not(.no-search)').each(function () { if ($(this).find('input[type=text]').length > 0) { return } let $menu = $('
').addClass('choice-options-menu') let $inp_search = $('').addClass('form-control').attr('type', 'text').attr('placeholder', gettext('Search query')) $menu.append($inp_search) $(this).prepend($menu) $inp_search.on('keyup change', function (e) { let term = $inp_search.val().toLowerCase() $(this).closest('.scrolling-choice').find('div.radio').each(function () { $(this).toggleClass('hidden', !$(this).text().toLowerCase().includes(term)) }) }) }) el.find('div.scrolling-multiple-choice').each(function () { if ($(this).find('.choice-options-all').length > 0) { return } let $menu = $('
').addClass('choice-options-menu') let $a_all = $('').addClass('choice-options-all').attr('href', '#').text(gettext('All')) let $a_none = $('').addClass('choice-options-none').attr('href', '#').text(gettext('None')) let $inp_search = $('').addClass('form-control').attr('type', 'text').attr('placeholder', gettext('Search query')) let $lbl_tgl = $('
').addClass('checkbox menu-checkbox') let $cb_tgl = $('').attr('type', 'checkbox').addClass('menu-checkbox') $lbl_tgl.append($('
').text(res.text).html()) ) if (res.event) { $ret.append( $('').addClass('secondary').append( $('').addClass('fa fa-calendar fa-fw') ).append(' ').append($('
').text(res.event).html()) ) } return $ret }, }).on('select2:select', function () { // Allow continuing to select if ($s[0].hasAttribute('multiple')) { window.setTimeout(function () { $s.parent().find('.select2-search__field').focus() }, 50) } }) if ($s[0].hasAttribute('data-close-on-clear')) { $s.on('select2:clear', function () { window.setTimeout(function () { $s.select2('close') }, 50) }) } }) el.find('[data-model-select2=event]').each(function () { let $s = $(this) $s.select2({ closeOnSelect: !this.hasAttribute('multiple'), theme: 'bootstrap', delay: 100, allowClear: !$s.prop('required'), width: '100%', language: $('body').attr('data-select2-locale'), ajax: { url: $(this).attr('data-select2-url'), data: function (params) { return { query: params.term, page: params.page || 1 } } }, placeholder: $(this).attr('data-placeholder') || '', templateResult: function (res) { if (!res.id) { return res.text } let $ret = $('').append( $('').addClass('event-name-full').append($('
').text(res.name).html()) ) if (res.organizer) { $ret.append( $('').addClass('event-organizer').append( $('').addClass('fa fa-users fa-fw') ).append(' ').append($('
').text(res.organizer).html()) ) } if (res.date_range) { $ret.append( $('').addClass('event-daterange').append( $('').addClass('fa fa-calendar fa-fw') ).append(' ').append(res.date_range) ) } return $ret }, }).on('select2:select', function () { // Allow continuing to select window.setTimeout(function () { $s.parent().find('.select2-search__field').focus() }, 50) }) }) el.find('.simple-subevent-choice').change(function () { $(this).closest('form').submit() }) el.find('input[name=basics-slug]').bind('keyup keydown change', function () { $(this).closest('.form-group').find('.slug-length').toggle($(this).val().length > 16) }) el.find('script[data-replace-with-qr]').each(function () { let $div = $('
') let qrText = this.getAttribute('type') === 'application/json' && this.textContent.startsWith('"') ? JSON.parse(this.textContent) : $(this).html() $div.insertBefore($(this)) $div.qrcode( { text: qrText, correctLevel: 0, // M width: $(this).attr('data-size') ? parseInt($(this).attr('data-size')) : 256, height: $(this).attr('data-size') ? parseInt($(this).attr('data-size')) : 256, } ) }) el.find('.bulk-edit-field-group').each(function () { let $checkbox = $(this).find('input[type=checkbox][name=_bulk]') let $content = $(this).find('.field-content') let $fields = $content.find('input, select, textarea, button') let $dialog = $(this).attr('data-confirm-dialog') ? $($(this).attr('data-confirm-dialog')) : null let warningShown = false if ($dialog) { $dialog.on('close', function () { if ($dialog.get(0).returnValue === 'yes') { $checkbox.prop('checked', true) } else { $checkbox.prop('checked', false) warningShown = false } update() }) } var update = function () { let isChecked = $checkbox.prop('checked') $content.toggleClass('enabled', isChecked) $fields.attr('tabIndex', isChecked ? 0 : -1) } $content.on('focusin change click', function () { if ($checkbox.prop('checked')) return if ($dialog && !warningShown) { warningShown = true $dialog.get(0).showModal() } else { $checkbox.prop('checked', true) update() } }) $checkbox.on('change', function () { let isChecked = $checkbox.prop('checked') if (isChecked && $dialog && !warningShown) { warningShown = true $dialog.get(0).showModal() } else if (!isChecked) { warningShown = false } update() }) update() }) el.find('input[name*=question], select[name*=question]').change(questions_toggle_dependent) questions_toggle_dependent() questions_init_photos(el) let lastFocusedInput $(document).on('focusin', 'input, textarea', function (e) { lastFocusedInput = e.target }).on('click', function (e) { if (e.target.classList.contains('content-placeholder')) { let container = e.target.closest('.form-group') if (!lastFocusedInput || !container.contains(lastFocusedInput)) { lastFocusedInput = container.querySelector('input, textarea') // lastFocusedInput.selectionStart = lastFocusedInput.selectionEnd = lastFocusedInput.value.length; } if (lastFocusedInput) { let start = lastFocusedInput.selectionStart let end = lastFocusedInput.selectionEnd let v = lastFocusedInput.value let p = e.target.textContent let phStart = /\{\w*$/.exec(v.substring(0, start)) let phEnd = /^\w*\}/.exec(v.substring(end)) if (phStart) { start -= phStart[0].length } if (phEnd) { end += phEnd[0].length } lastFocusedInput.value = v.substring(0, start) + p + v.substring(end) lastFocusedInput.selectionStart = start lastFocusedInput.selectionEnd = start + p.length lastFocusedInput.focus() } } }) } function setup_basics (el) { el.find('#sumtoggle').find('button').click(function () { $('.table-product-overview .sum-gross').toggle($(this).attr('data-target') === '.sum-gross') $('.table-product-overview .sum-net').toggle($(this).attr('data-target') === '.sum-net') $('.table-product-overview .count').toggle($(this).attr('data-target') === '.count') $('#sumtoggle').find('button').not($(this)).removeClass('active') $(this).addClass('active') }) el.find('.collapsible').collapse() el.find('input[data-toggle=radiocollapse]').change(function () { $($(this).attr('data-parent')).find('.collapse.in').collapse('hide') $($(this).attr('data-target')).collapse('show') }) el.find('div.collapsed').removeClass('collapsed').addClass('collapse') el.find('.has-error, .panel-body .alert-danger:not(:has(.has-error))').each(function () { let $this = $(this) let $panel = $this.closest('div.panel-collapse').collapse('show') let alert = el.find('.alert-danger').get(0) if (alert === this) { return } let label = '' let description = '' let scrollTarget = null if ($this.hasClass('alert-danger')) { // just a general error messages without a actual errorenous input label = $this.closest('.panel').find('.panel-title').contents().filter(function () { return this.nodeType == Node.TEXT_NODE }).text() description = $this.text() scrollTarget = $this.closest('.panel').get(0) if (!scrollTarget.id) { scrollTarget.id = 'panel_' + $('input', scrollTarget).attr('id') } } else { label = $('label', this).first().contents().filter(function () { return this.nodeType != Node.ELEMENT_NODE || !this.classList.contains('optional') }).text() description = $('.help-block', this).first().text() scrollTarget = $(':input', this).get(0) } if (!alert || !scrollTarget) { return } $('
  • ') .append( $('
    ').attr("href", "#" + scrollTarget.id).text($.trim(label)) ) .append( document.createTextNode(' - ' + description) ) .appendTo(alert.querySelector('ul') || $('