forked from CGM_Public/pretix_original
1130 lines
37 KiB
JavaScript
1130 lines
37 KiB
JavaScript
/* 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('<div class=\'help-block contrast-state\'></div>')
|
|
}
|
|
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('<span class=\'fa fa-fw ' + icon + '\'></span>')
|
|
.append(text)
|
|
$note.removeClass('text-success').removeClass('text-danger').addClass(cls)
|
|
} else {
|
|
$icon.html('<span class=\'fa fa-fw ' + icon + ' ' + cls + '\'></span>')
|
|
$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 = $('<div>').addClass('choice-options-menu')
|
|
let $inp_search = $('<input>').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 = $('<div>').addClass('choice-options-menu')
|
|
let $a_all = $('<a>').addClass('choice-options-all').attr('href', '#').text(gettext('All'))
|
|
let $a_none = $('<a>').addClass('choice-options-none').attr('href', '#').text(gettext('None'))
|
|
let $inp_search = $('<input>').addClass('form-control').attr('type', 'text').attr('placeholder', gettext('Search query'))
|
|
let $lbl_tgl = $('<div>').addClass('checkbox menu-checkbox')
|
|
let $cb_tgl = $('<input>').attr('type', 'checkbox').addClass('menu-checkbox')
|
|
$lbl_tgl.append($('<label>').append($cb_tgl).append(gettext('Selected only')))
|
|
$menu.append($('<span>').append($a_all).append(' / ').append($a_none))
|
|
if (!$(this).is('.no-search')) {
|
|
$menu.append($inp_search)
|
|
$menu.append($lbl_tgl)
|
|
}
|
|
$(this).prepend($menu)
|
|
|
|
$(this).find('.choice-options-none').click(function (e) {
|
|
$(this).closest('.scrolling-multiple-choice').find('input[type=checkbox]:not(.menu-checkbox)').prop('checked', false)
|
|
$cb_tgl.trigger('change')
|
|
e.preventDefault()
|
|
return false
|
|
})
|
|
$(this).find('.choice-options-all').click(function (e) {
|
|
$(this).closest('.scrolling-multiple-choice').find('input[type=checkbox]:not(.menu-checkbox)').prop('checked', true)
|
|
$cb_tgl.trigger('change')
|
|
e.preventDefault()
|
|
return false
|
|
})
|
|
$cb_tgl.on('change', function (e) {
|
|
let tgl = $cb_tgl.prop('checked')
|
|
|
|
$(this).closest('.scrolling-multiple-choice').find('div.checkbox:not(.menu-checkbox)').each(function () {
|
|
$(this).toggleClass('sr-only', tgl && !$(this).find('input[type=checkbox]').prop('checked'))
|
|
})
|
|
})
|
|
$inp_search.on('keyup change', function (e) {
|
|
let term = $inp_search.val().toLowerCase()
|
|
|
|
$(this).closest('.scrolling-multiple-choice').find('div.checkbox:not(.menu-checkbox)').each(function () {
|
|
$(this).toggleClass('hidden', !$(this).text().toLowerCase().includes(term))
|
|
})
|
|
})
|
|
})
|
|
|
|
el.find('.select2-static').select2({
|
|
theme: 'bootstrap',
|
|
language: $('body').attr('data-select2-locale'),
|
|
})
|
|
|
|
el.find('[data-model-select2=json_script]').each(function () {
|
|
const selectedValue = this.value
|
|
this.replaceChildren()
|
|
$(this).select2({
|
|
theme: 'bootstrap',
|
|
language: $('body').attr('data-select2-locale'),
|
|
data: JSON.parse($(this.getAttribute('data-select2-src')).text()),
|
|
width: '100%',
|
|
}).val(selectedValue).trigger('change')
|
|
})
|
|
|
|
el.find('input[data-typeahead-url]').each(function () {
|
|
let $inp = $(this)
|
|
if ($inp.data('ttTypeahead') || $inp.hasClass('tt-hint')) {
|
|
// Already initialized on this element
|
|
return
|
|
}
|
|
$inp.typeahead(null, {
|
|
minLength: 1,
|
|
highlight: true,
|
|
source: new Bloodhound({
|
|
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
|
|
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
|
remote: {
|
|
url: $inp.attr('data-typeahead-url'),
|
|
prepare: function (query, settings) {
|
|
let sep = (settings.url.indexOf('?') > 0) ? '&' : '?'
|
|
settings.url = settings.url + sep + 'q=' + encodeURIComponent(query)
|
|
return settings
|
|
},
|
|
transform: function (object) {
|
|
let results = object.results
|
|
let suggs = []
|
|
let reslen = results.length
|
|
for (let i = 0; i < reslen; i++) {
|
|
suggs.push(results[i])
|
|
}
|
|
return suggs
|
|
}
|
|
}
|
|
}),
|
|
display: function (obj) {
|
|
return obj.name
|
|
},
|
|
})
|
|
})
|
|
|
|
el.find('[data-model-select2=generic]').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'),
|
|
placeholder: $(this).attr('data-placeholder') || '',
|
|
ajax: {
|
|
url: $(this).attr('data-select2-url'),
|
|
data: function (params) {
|
|
return {
|
|
query: params.term,
|
|
page: params.page || 1
|
|
}
|
|
}
|
|
},
|
|
templateResult: function (res) {
|
|
if (!res.id) {
|
|
return res.text
|
|
}
|
|
let $ret = $('<span>').append(
|
|
$(res.inactive ? '<strike class=\'text-muted\'>' : '<span>').addClass('primary').append($('<div>').text(res.text).html())
|
|
)
|
|
if (res.event) {
|
|
$ret.append(
|
|
$('<span>').addClass('secondary').append(
|
|
$('<span>').addClass('fa fa-calendar fa-fw')
|
|
).append(' ').append($('<div>').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 = $('<span>').append(
|
|
$('<span>').addClass('event-name-full').append($('<div>').text(res.name).html())
|
|
)
|
|
if (res.organizer) {
|
|
$ret.append(
|
|
$('<span>').addClass('event-organizer').append(
|
|
$('<span>').addClass('fa fa-users fa-fw')
|
|
).append(' ').append($('<div>').text(res.organizer).html())
|
|
)
|
|
}
|
|
if (res.date_range) {
|
|
$ret.append(
|
|
$('<span>').addClass('event-daterange').append(
|
|
$('<span>').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 = $('<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
|
|
}
|
|
|
|
$('<li></li>')
|
|
.append(
|
|
$('<a></a>').attr("href", "#" + scrollTarget.id).text($.trim(label))
|
|
)
|
|
.append(
|
|
document.createTextNode(' - ' + description)
|
|
)
|
|
.appendTo(alert.querySelector('ul') || $('<ul>').appendTo(alert))
|
|
.find('a').on('click', function (e) {
|
|
$panel.collapse('show')
|
|
let tab = scrollTarget.closest('.tab-pane')
|
|
if (tab) {
|
|
$('.nav-tabs a[href=\'#' + tab.id + '\']').click()
|
|
}
|
|
scrollTarget.scrollIntoView()
|
|
scrollTarget.focus()
|
|
})
|
|
})
|
|
|
|
el.find('[data-toggle="tooltip"]').tooltip()
|
|
el.find('[data-toggle="tooltip_html"]').tooltip({
|
|
html: true,
|
|
whiteList: {
|
|
// Global attributes allowed on any supplied element below.
|
|
'*': ['class', 'dir', 'id', 'lang', 'role'],
|
|
b: [],
|
|
br: [],
|
|
code: [],
|
|
div: [], // required for template
|
|
h3: ['class', 'role'], // required for template
|
|
i: [],
|
|
small: [],
|
|
span: [],
|
|
strong: [],
|
|
u: [],
|
|
}
|
|
})
|
|
|
|
el.find('a.pagination-selection').click(function (e) {
|
|
e.preventDefault()
|
|
let max = parseInt($(this).data('max'))
|
|
let inp = prompt(gettext('Enter page number between 1 and %(max)s.').replace('%(max)s', max))
|
|
if (inp) {
|
|
if (!parseInt(inp) || parseInt(inp) < 1 || parseInt(inp) > max) {
|
|
alert(gettext('Invalid page number.'))
|
|
} else {
|
|
location.href = $(this).attr('data-href').replace('_PAGE_', inp)
|
|
}
|
|
}
|
|
})
|
|
|
|
let url = document.location.toString()
|
|
if (url.match('#')) {
|
|
$('.nav-tabs a[href="#' + url.split('#')[1] + '"]').tab('show')
|
|
}
|
|
el.find('a[data-toggle="tab"]').on('click', function (e) {
|
|
if (!$(this).closest('.tab-content').length) {
|
|
// only append hash if not inside a .panel
|
|
window.location.hash = this.hash
|
|
}
|
|
})
|
|
|
|
// Event wizard
|
|
el.find('#event-slug-random-generate').click(function () {
|
|
let url = $(this).attr('data-rng-url')
|
|
$('#id_basics-slug').val('Generating...')
|
|
$.getJSON(url, function (data) {
|
|
$('#id_basics-slug').val(data.slug)
|
|
})
|
|
})
|
|
|
|
el.find('.propagated-settings-box').find('input, textarea, select').not('[readonly]')
|
|
.attr('data-propagated-locked', 'true').prop('readonly', true)
|
|
|
|
el.find('.propagated-settings-box button[data-action=unlink]').click(function (ev) {
|
|
let $box = $(this).closest('.propagated-settings-box')
|
|
$box.find('input[name=decouple]').val($(this).val())
|
|
$box.find('[data-propagated-locked]').prop('readonly', false)
|
|
$box.removeClass('locked').addClass('unlocked')
|
|
ev.preventDefault()
|
|
return true
|
|
})
|
|
|
|
// Tables with bulk selection, e.g. subevent list
|
|
el.find('input[data-toggle-table]').each(function (ev) {
|
|
let $toggle = $(this)
|
|
let $actionButtons = $('.batch-select-actions button', this.form)
|
|
let countLabels = $('<span></span>').appendTo($actionButtons.filter(function () { return !$(this).closest('.dropdown-menu').length }))
|
|
let $table = $toggle.closest('table')
|
|
let $selectAll = $table.find('.table-select-all')
|
|
let $rows = $table.find('tbody tr')
|
|
let $checkboxes = $rows.find('td:first-child input[type=checkbox]')
|
|
let firstIndex, lastIndex, selectionChecked, onChangeSelectionHappened = false
|
|
let updateSelection = function (a, b, checked) {
|
|
if (a > b) {
|
|
// [a, b] = [b, a];// ES6 not ready yet for pretix
|
|
let tmp = a
|
|
a = b
|
|
b = tmp
|
|
}
|
|
for (let i = a; i <= b; i++) {
|
|
let checkbox = $checkboxes.get(i)
|
|
if (!checkbox.hasAttribute('data-inital')) checkbox.setAttribute('data-inital', checkbox.checked)
|
|
if (checked === undefined || checked === null) checkbox.checked = checkbox.getAttribute('data-inital') === 'true'
|
|
else checkbox.checked = checked
|
|
}
|
|
}
|
|
let onChangeSelection = function (ev) {
|
|
onChangeSelectionHappened = true
|
|
|
|
let row = ev.target.closest('tr')
|
|
let currentIndex = 0
|
|
while (row = row.previousSibling) {
|
|
if (row.tagName) currentIndex++
|
|
}
|
|
let dCurrent = currentIndex - firstIndex
|
|
let dLast = lastIndex - firstIndex
|
|
if (dCurrent * dLast < 0) {
|
|
// direction of selection changed => reset all previously selected
|
|
updateSelection(lastIndex, firstIndex)
|
|
} else if (Math.abs(dCurrent) < Math.abs(dLast)) {
|
|
// selection distance decreased => reset unselected
|
|
updateSelection(currentIndex, lastIndex)
|
|
}
|
|
lastIndex = currentIndex
|
|
updateSelection(firstIndex, currentIndex, selectionChecked)
|
|
|
|
ev.preventDefault()
|
|
}
|
|
$table.on('pointerdown', function (ev) {
|
|
if (!ev.target.closest('td:first-child')) return
|
|
let row = ev.target.closest('tr')
|
|
selectionChecked = !row.querySelector('td:first-child input').checked
|
|
|
|
firstIndex = 0
|
|
while (row = row.previousSibling) {
|
|
if (row.tagName) firstIndex++
|
|
}
|
|
lastIndex = firstIndex
|
|
|
|
ev.preventDefault()
|
|
$rows.on('pointerenter', onChangeSelection)
|
|
|
|
$(document).one('pointerup', function (ev) {
|
|
if (onChangeSelectionHappened) {
|
|
ev.preventDefault()
|
|
onChangeSelectionHappened = false
|
|
$checkboxes.removeAttr('data-inital')
|
|
|
|
update()
|
|
}
|
|
$rows.off('pointerenter', onChangeSelection)
|
|
})
|
|
})
|
|
|
|
var update = function () {
|
|
let nrOfChecked = $checkboxes.filter(':checked').length
|
|
let allChecked = nrOfChecked == $checkboxes.length
|
|
|
|
if (!nrOfChecked) countLabels.empty()
|
|
else countLabels.text(' (' + nrOfChecked + ')')
|
|
|
|
if (!allChecked) $selectAll.find('input').prop('checked', false)
|
|
|
|
$actionButtons.attr('disabled', !nrOfChecked)
|
|
$toggle.prop('checked', allChecked).prop('indeterminate', nrOfChecked > 0 && !allChecked)
|
|
$selectAll.toggleClass('hidden', nrOfChecked !== $checkboxes.length).prop('hidden', nrOfChecked !== $checkboxes.length)
|
|
}
|
|
|
|
$checkboxes.change(update)
|
|
$toggle.change(function (ev) {
|
|
this.indeterminate = false
|
|
$checkboxes.prop('checked', this.checked)
|
|
update()
|
|
})
|
|
$selectAll.find('input').change(function (ev) {
|
|
if (this.checked) countLabels.text(' (' + this.getAttribute('data-results-total') + ')')
|
|
else countLabels.text(' (' + $checkboxes.filter(':checked').length + ')')
|
|
})
|
|
|
|
update()
|
|
})
|
|
|
|
// Items and categories
|
|
el.find('.internal-name-wrapper').each(function () {
|
|
if ($(this).find('input').val() === '') {
|
|
let $fg = $(this).find('.form-group')
|
|
$fg.hide()
|
|
var $fgl = $('<div>').addClass('form-group').append(
|
|
$('<div>').addClass('col-md-9 col-md-offset-3').append(
|
|
$('<div>').addClass('help-block').append(
|
|
$('<a>').attr('href', '#').text(
|
|
gettext('Use a different name internally')
|
|
).click(function () {
|
|
$fg.slideDown()
|
|
$fgl.slideUp()
|
|
return false
|
|
})
|
|
)
|
|
)
|
|
)
|
|
$(this).append($fgl)
|
|
}
|
|
})
|
|
|
|
el.find('button[data-toggle=qrcode]').click(function (e) {
|
|
e.preventDefault()
|
|
let $current = $('.qr-code-overlay[data-qrcode=\'' + $(this).attr('data-qrcode') + '\']')
|
|
if ($current.length) {
|
|
$('.qr-code-overlay').attr('data-qrcode', '').slideUp(200)
|
|
return false
|
|
}
|
|
$('.qr-code-overlay').remove()
|
|
let $div = $('<div>').addClass('qr-code-overlay').attr('data-qrcode', $(this).attr('data-qrcode'))
|
|
$div.appendTo($('body'))
|
|
let offset = $(this).offset()
|
|
$div.css('top', offset.top + $(this).outerHeight() + 10).css('left', offset.left)
|
|
let $child = $('<div>')
|
|
$child.appendTo($div)
|
|
$child.qrcode(
|
|
{
|
|
text: $(this).attr('data-qrcode'),
|
|
correctLevel: 0, // M
|
|
width: 196,
|
|
height: 196
|
|
}
|
|
)
|
|
let $inner = $('<div>').text($(this).attr('data-qrcode').slice(0, 10) + '…')
|
|
$inner.append($('<btn>').addClass('btn btn-link btn-xs btn-clipboard').attr('data-clipboard-text', $(this).attr('data-qrcode')).append(
|
|
$('<span>').addClass('fa fa-clipboard').attr('aria-hidden', 'true')
|
|
))
|
|
$div.append($inner.get(0).innerHTML + '<br>')
|
|
$div.append(gettext('Click to close'))
|
|
$div.slideDown(200)
|
|
$div.click(function (e) {
|
|
if ($(e.target).closest('.btn').length) {
|
|
return
|
|
}
|
|
$('.qr-code-overlay').attr('data-qrcode', '').slideUp(200)
|
|
})
|
|
return false
|
|
})
|
|
}
|
|
|
|
$(function () {
|
|
'use strict'
|
|
|
|
$('body').removeClass('nojs')
|
|
lightbox.init()
|
|
|
|
$(document).on('click', '.variations .variations-select-all', function (e) {
|
|
$(this).parent().parent().find('input[type=checkbox]').prop('checked', true).change()
|
|
e.stopPropagation()
|
|
return false
|
|
})
|
|
$(document).on('click', '.variations .variations-select-none', function (e) {
|
|
$(this).parent().parent().find('input[type=checkbox]').prop('checked', false).change()
|
|
e.stopPropagation()
|
|
return false
|
|
})
|
|
if ($('.items-on-quota').length) {
|
|
$('.items-on-quota .panel').each(function () {
|
|
let $panel = $(this)
|
|
$panel.toggleClass('panel-success', $panel.find('input:checked').length > 0)
|
|
$(this).find('input').change(function () {
|
|
$panel.toggleClass('panel-success', $panel.find('input:checked').length > 0)
|
|
})
|
|
})
|
|
}
|
|
|
|
setup_basics($('body'))
|
|
form_handlers($('body'))
|
|
$(document).trigger('pretix:bind-forms')
|
|
|
|
$('#ajaxerr').on('click', '.ajaxerr-close', ajaxErrDialog.hide)
|
|
moment.locale($('body').attr('data-datetimelocale'))
|
|
add_log_expand_handlers($('body'))
|
|
})
|
|
|
|
function add_log_expand_handlers (el) {
|
|
el.find('a[data-expandlogs], a[data-expandrefund], a[data-expandpayment]').click(function (e) {
|
|
e.preventDefault()
|
|
let $a = $(this)
|
|
let id = $(this).attr('data-id')
|
|
$a.find('.fa').removeClass('fa-eye').addClass('fa-cog fa-spin')
|
|
let url = '/control/logdetail/'
|
|
if ($a.is('[data-expandrefund]')) {
|
|
url += 'refund/'
|
|
} else if ($a.is('[data-expandpayment]')) {
|
|
url += 'payment/'
|
|
}
|
|
function format_data (data) {
|
|
return Object.entries(data).map(([key, value]) =>
|
|
$('<div>').append(
|
|
$('<b>').text(key + ': '),
|
|
$('<span>').text(JSON.stringify(value, null, 2))))
|
|
}
|
|
$.getJSON(url + '?pk=' + id, function (data) {
|
|
if ($a.parent().tagName === 'p') {
|
|
$('<pre>').append(format_data(data)).insertAfter($a.parent())
|
|
} else {
|
|
$('<pre>').append(format_data(data)).appendTo($a.parent())
|
|
}
|
|
$a.remove()
|
|
})
|
|
return false
|
|
})
|
|
}
|
|
|
|
$(function () {
|
|
$('form[method=post]').filter(function () {
|
|
return $(this).find('button:not([type=button]), input[type=submit]').length > 0
|
|
}).areYouSure({ message: gettext('You have unsaved changes!') })
|
|
})
|