kopia lustrzana https://github.com/wagtail/wagtail
398 wiersze
12 KiB
JavaScript
398 wiersze
12 KiB
JavaScript
/* global ModalWorkflow */
|
|
|
|
import $ from 'jquery';
|
|
import { initTabs } from './tabs';
|
|
import { gettext } from '../utils/gettext';
|
|
import { WAGTAIL_CONFIG } from '../config/wagtailConfig';
|
|
|
|
const validateCreationForm = (form) => {
|
|
let hasErrors = false;
|
|
form.querySelectorAll('input[required]').forEach((input) => {
|
|
if (!input.value) {
|
|
hasErrors = true;
|
|
if (!input.hasAttribute('aria-invalid')) {
|
|
input.setAttribute('aria-invalid', 'true');
|
|
const field = input.closest('[data-field]');
|
|
field.classList.add('w-field--error');
|
|
const errors = field.querySelector('[data-field-errors]');
|
|
const icon = errors.querySelector('.icon');
|
|
if (icon) {
|
|
icon.removeAttribute('hidden');
|
|
}
|
|
const errorElement = document.createElement('p');
|
|
errorElement.classList.add('error-message');
|
|
errorElement.textContent = gettext('This field is required.');
|
|
errors.appendChild(errorElement);
|
|
}
|
|
}
|
|
});
|
|
if (hasErrors) {
|
|
setTimeout(() => {
|
|
// clear any loading state on progress buttons
|
|
const attr = 'data-w-progress-loading-value';
|
|
form.querySelectorAll(`[${attr}~="true"]`).forEach((element) => {
|
|
element.removeAttribute(attr);
|
|
});
|
|
}, 500);
|
|
}
|
|
return !hasErrors;
|
|
};
|
|
|
|
const submitCreationForm = (modal, form, { errorContainerSelector }) => {
|
|
const formdata = new FormData(form);
|
|
|
|
$.ajax({
|
|
url: form.action,
|
|
data: formdata,
|
|
processData: false,
|
|
contentType: false,
|
|
type: 'POST',
|
|
dataType: 'text',
|
|
success: modal.loadResponseText,
|
|
error(response, textStatus, errorThrown) {
|
|
const message =
|
|
gettext(
|
|
'Report this error to your website administrator with the following information:',
|
|
) +
|
|
'<br />' +
|
|
errorThrown +
|
|
' - ' +
|
|
response.status;
|
|
$(errorContainerSelector, modal.body).append(
|
|
'<div class="help-block help-critical">' +
|
|
'<strong>' +
|
|
gettext('Server Error') +
|
|
': </strong>' +
|
|
message +
|
|
'</div>',
|
|
);
|
|
},
|
|
});
|
|
};
|
|
|
|
class SearchController {
|
|
constructor(opts) {
|
|
this.form = opts.form;
|
|
this.containerElement = opts.containerElement;
|
|
this.onLoadResults = opts.onLoadResults;
|
|
this.resultsContainer = $(
|
|
opts.resultsContainerSelector,
|
|
this.containerElement,
|
|
);
|
|
this.inputDelay = opts.inputDelay || 200;
|
|
|
|
this.searchUrl = this.form.attr('action');
|
|
this.request = null;
|
|
|
|
this.form.on('submit', () => {
|
|
this.searchFromForm();
|
|
return false;
|
|
});
|
|
}
|
|
|
|
attachSearchInput(selector) {
|
|
let timer;
|
|
|
|
$(selector, this.containerElement).on('input', () => {
|
|
if (this.request) {
|
|
this.request.abort();
|
|
}
|
|
clearTimeout(timer);
|
|
timer = setTimeout(() => {
|
|
this.searchFromForm();
|
|
}, this.inputDelay);
|
|
});
|
|
}
|
|
|
|
attachSearchFilter(selector) {
|
|
$(selector, this.containerElement).on('change', () => {
|
|
this.searchFromForm();
|
|
});
|
|
}
|
|
|
|
fetchResults(url, queryParams) {
|
|
const requestOptions = {
|
|
url: url,
|
|
success: (resultsData) => {
|
|
this.request = null;
|
|
this.resultsContainer.html(resultsData);
|
|
if (this.onLoadResults) {
|
|
this.onLoadResults(this.resultsContainer);
|
|
}
|
|
},
|
|
error() {
|
|
this.request = null;
|
|
},
|
|
};
|
|
if (queryParams) {
|
|
requestOptions.data = queryParams;
|
|
}
|
|
this.request = $.ajax(requestOptions);
|
|
}
|
|
|
|
search(queryParams) {
|
|
this.fetchResults(this.searchUrl, queryParams);
|
|
}
|
|
|
|
searchFromForm() {
|
|
this.search(this.form.serialize());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use w-sync/w-clean Stimulus controllers instead
|
|
* Temporary bridge for third-party code using filename-based title prefill
|
|
*/
|
|
function initPrefillTitleFromFilename({
|
|
fileInput,
|
|
titleInput,
|
|
creationFormEventName,
|
|
}) {
|
|
if (creationFormEventName) {
|
|
const form = fileInput.closest('form');
|
|
if (form) {
|
|
form.addEventListener('change', (e) => {
|
|
if (e.target === fileInput) {
|
|
form.dispatchEvent(
|
|
new CustomEvent(creationFormEventName, {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
detail: {
|
|
data: { title: '' }, // Let SyncController populate this
|
|
filename: fileInput.value
|
|
.split('\\')
|
|
.pop()
|
|
.replace(/\.[^.]+$/, ''),
|
|
maxTitleLength: titleInput?.getAttribute('maxLength') || null,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
/*
|
|
console.warn(
|
|
'initPrefillTitleFromFilename is deprecated. Please use Stimulus controllers (w-sync, w-clean) instead.',
|
|
);
|
|
*/
|
|
}
|
|
|
|
// Deprecated: for legacy support only
|
|
window.initPrefillTitleFromFilename = initPrefillTitleFromFilename;
|
|
|
|
class ChooserModalOnloadHandlerFactory {
|
|
constructor(opts) {
|
|
this.chooseStepName = opts?.chooseStepName || 'choose';
|
|
this.chosenStepName = opts?.chosenStepName || 'chosen';
|
|
this.reshowCreationFormStepName =
|
|
opts?.reshowCreationFormStepName || 'reshow_creation_form';
|
|
this.chosenLinkSelector =
|
|
opts?.chosenLinkSelector || 'a[data-chooser-modal-choice]';
|
|
this.paginationLinkSelector =
|
|
opts?.paginationLinkSelector || '.pagination a';
|
|
this.searchFormSelector =
|
|
opts?.searchFormSelector || 'form[data-chooser-modal-search]';
|
|
this.resultsContainerSelector =
|
|
opts?.resultsContainerSelector || '#search-results';
|
|
this.searchInputSelectors = opts?.searchInputSelectors || ['#id_q'];
|
|
this.searchFilterSelectors = opts?.searchFilterSelectors || [
|
|
'[data-chooser-modal-search-filter]',
|
|
];
|
|
this.chosenResponseName = opts?.chosenResponseName || 'chosen';
|
|
this.searchInputDelay = opts?.searchInputDelay || 200;
|
|
this.creationFormSelector =
|
|
opts?.creationFormSelector || 'form[data-chooser-modal-creation-form]';
|
|
this.creationFormTabSelector =
|
|
opts?.creationFormTabSelector || '#tab-create';
|
|
this.creationFormFileFieldSelector = opts?.creationFormFileFieldSelector;
|
|
this.creationFormTitleFieldSelector = opts?.creationFormTitleFieldSelector;
|
|
this.creationFormEventName = opts?.creationFormEventName;
|
|
|
|
this.searchController = null;
|
|
}
|
|
|
|
ajaxifyLinks(modal, containerElement) {
|
|
if (!this.searchController) {
|
|
throw new Error(
|
|
'Cannot call ajaxifyLinks until a SearchController is set up',
|
|
);
|
|
}
|
|
|
|
$(this.chosenLinkSelector, containerElement).on('click', (event) => {
|
|
modal.loadUrl(event.currentTarget.href);
|
|
return false;
|
|
});
|
|
|
|
$(this.paginationLinkSelector, containerElement).on('click', (event) => {
|
|
this.searchController.fetchResults(event.currentTarget.href);
|
|
return false;
|
|
});
|
|
|
|
// Reinitialize tabs to hook up tab event listeners in the modal
|
|
if (this.modalHasTabs(modal)) initTabs();
|
|
|
|
this.updateMultipleChoiceSubmitEnabledState(modal);
|
|
$('[data-multiple-choice-select]', containerElement).on('change', () => {
|
|
this.updateMultipleChoiceSubmitEnabledState(modal);
|
|
});
|
|
}
|
|
|
|
updateMultipleChoiceSubmitEnabledState(modal) {
|
|
// update the enabled state of the multiple choice submit button depending on whether
|
|
// any items have been selected
|
|
if ($('[data-multiple-choice-select]:checked', modal.body).length) {
|
|
$('[data-multiple-choice-submit]', modal.body).removeAttr('disabled');
|
|
} else {
|
|
$('[data-multiple-choice-submit]', modal.body).attr('disabled', true);
|
|
}
|
|
}
|
|
|
|
modalHasTabs(modal) {
|
|
return $('[data-tabs]', modal.body).length;
|
|
}
|
|
|
|
ajaxifyCreationForm(modal) {
|
|
/* Convert the creation form to an AJAX submission */
|
|
$(this.creationFormSelector, modal.body).on('submit', (event) => {
|
|
if (validateCreationForm(event.currentTarget)) {
|
|
submitCreationForm(modal, event.currentTarget, {
|
|
errorContainerSelector: this.creationFormTabSelector,
|
|
});
|
|
}
|
|
return false;
|
|
});
|
|
|
|
if (
|
|
this.creationFormFileFieldSelector &&
|
|
this.creationFormTitleFieldSelector
|
|
) {
|
|
const fileField = $(this.creationFormFileFieldSelector, modal.body);
|
|
const titleField = $(this.creationFormTitleFieldSelector, modal.body);
|
|
|
|
fileField.attr({
|
|
'data-controller': 'w-sync',
|
|
'data-action': 'change->w-sync#apply',
|
|
'data-w-sync-target-value': this.creationFormTitleFieldSelector,
|
|
'data-w-sync-normalize-value': 'true',
|
|
'data-w-sync-name-value': this.creationFormEventName,
|
|
});
|
|
|
|
titleField.attr({
|
|
'data-controller': 'w-clean',
|
|
'data-action': 'blur->w-clean#slugify',
|
|
});
|
|
}
|
|
}
|
|
|
|
initSearchController(modal) {
|
|
this.searchController = new SearchController({
|
|
form: $(this.searchFormSelector, modal.body),
|
|
containerElement: modal.body,
|
|
resultsContainerSelector: this.resultsContainerSelector,
|
|
onLoadResults: (containerElement) => {
|
|
this.ajaxifyLinks(modal, containerElement);
|
|
},
|
|
inputDelay: this.searchInputDelay,
|
|
});
|
|
this.searchInputSelectors.forEach((selector) => {
|
|
this.searchController.attachSearchInput(selector);
|
|
});
|
|
this.searchFilterSelectors.forEach((selector) => {
|
|
this.searchController.attachSearchFilter(selector);
|
|
});
|
|
}
|
|
|
|
onLoadChooseStep(modal) {
|
|
this.initSearchController(modal);
|
|
this.ajaxifyLinks(modal, modal.body);
|
|
this.ajaxifyCreationForm(modal);
|
|
// Set up submissions of the "choose multiple items" form to open in the modal.
|
|
modal.ajaxifyForm($('form[data-multiple-choice-form]', modal.body));
|
|
}
|
|
|
|
onLoadChosenStep(modal, jsonData) {
|
|
modal.respond(this.chosenResponseName, jsonData.result);
|
|
modal.close();
|
|
}
|
|
|
|
onLoadReshowCreationFormStep(modal, jsonData) {
|
|
$(this.creationFormTabSelector, modal.body).replaceWith(
|
|
jsonData.htmlFragment,
|
|
);
|
|
if (this.modalHasTabs(modal)) initTabs();
|
|
this.ajaxifyCreationForm(modal);
|
|
}
|
|
|
|
getOnLoadHandlers() {
|
|
return {
|
|
[this.chooseStepName]: (modal, jsonData) => {
|
|
this.onLoadChooseStep(modal, jsonData);
|
|
},
|
|
[this.chosenStepName]: (modal, jsonData) => {
|
|
this.onLoadChosenStep(modal, jsonData);
|
|
},
|
|
[this.reshowCreationFormStepName]: (modal, jsonData) => {
|
|
this.onLoadReshowCreationFormStep(modal, jsonData);
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
const chooserModalOnloadHandlers =
|
|
new ChooserModalOnloadHandlerFactory().getOnLoadHandlers();
|
|
|
|
class ChooserModal {
|
|
onloadHandlers = chooserModalOnloadHandlers;
|
|
chosenResponseName = 'chosen'; // identifier for the ModalWorkflow response that indicates an item was chosen
|
|
|
|
constructor(baseUrl) {
|
|
this.baseUrl = baseUrl;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
getURL(opts) {
|
|
return this.baseUrl;
|
|
}
|
|
|
|
getURLParams(opts) {
|
|
const urlParams = {};
|
|
if (opts.multiple) {
|
|
urlParams.multiple = 1;
|
|
}
|
|
if (opts.linkedFieldFilters) {
|
|
Object.assign(urlParams, opts.linkedFieldFilters);
|
|
}
|
|
if (WAGTAIL_CONFIG.ACTIVE_CONTENT_LOCALE) {
|
|
// The user is editing a piece of translated content.
|
|
// Pass the locale along as a request parameter. If this
|
|
// model is also translatable, the results will be
|
|
// pre-filtered by this locale.
|
|
urlParams.locale = WAGTAIL_CONFIG.ACTIVE_CONTENT_LOCALE;
|
|
}
|
|
return urlParams;
|
|
}
|
|
|
|
open(opts, callback) {
|
|
ModalWorkflow({
|
|
url: this.getURL(opts || {}),
|
|
urlParams: this.getURLParams(opts || {}),
|
|
onload: this.onloadHandlers,
|
|
responses: {
|
|
[this.chosenResponseName]: (result) => {
|
|
callback(result);
|
|
},
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
export {
|
|
validateCreationForm,
|
|
submitCreationForm,
|
|
SearchController,
|
|
ChooserModalOnloadHandlerFactory,
|
|
chooserModalOnloadHandlers,
|
|
ChooserModal,
|
|
};
|