This commit is contained in:
Loïc Guibert
2022-09-30 20:02:02 +01:00
commit 66dafc36c3
2561 changed files with 454489 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
import $ from 'jquery';
import Sortable from 'sortablejs';
let body = $('body');
class Template {
constructor(container) {
this.container = $(container);
if (this.getName() === undefined) {
this.container = this.container.closest('[data-grav-array-name]');
}
}
getName() {
return this.container.data('grav-array-name') || '';
}
getKeyPlaceholder() {
return this.container.data('grav-array-keyname') || 'Key';
}
getValuePlaceholder() {
return this.container.data('grav-array-valuename') || 'Value';
}
isValueOnly() {
return this.container.find('[data-grav-array-mode="value_only"]:first').length || false;
}
isTextArea() {
return this.container.data('grav-array-textarea') || false;
}
shouldBeDisabled() {
// check for toggleables, if field is toggleable and it's not enabled, render disabled
let toggle = this.container.closest('.form-field').find('[data-grav-field="toggleable"] input[type="checkbox"]');
return toggle.length && toggle.is(':not(:checked)');
}
getNewRow() {
let tpl = '';
const value = this.isTextArea()
? `<textarea ${this.shouldBeDisabled() ? 'disabled="disabled"' : ''} data-grav-array-type="value" name="" placeholder="${this.getValuePlaceholder()}"></textarea>`
: `<input ${this.shouldBeDisabled() ? 'disabled="disabled"' : ''} data-grav-array-type="value" type="text" name="" value="" placeholder="${this.getValuePlaceholder()}" />`;
if (this.isValueOnly()) {
tpl += `
<div class="form-row array-field-value_only" data-grav-array-type="row">
<span data-grav-array-action="sort" class="fa fa-bars"></span>
${value}
`;
} else {
tpl += `
<div class="form-row" data-grav-array-type="row">
<span data-grav-array-action="sort" class="fa fa-bars"></span>
<input ${this.shouldBeDisabled() ? 'disabled="disabled"' : ''} data-grav-array-type="key" type="text" value="" placeholder="${this.getKeyPlaceholder()}" />
${value}
`;
}
tpl += `
<span data-grav-array-action="rem" class="fa fa-minus"></span>
<span data-grav-array-action="add" class="fa fa-plus"></span>
</div>`;
return tpl;
}
}
export default class ArrayField {
constructor() {
body.on('input', '[data-grav-array-type="key"], [data-grav-array-type="value"]', (event) => this.actionInput(event));
body.on('click touch', '[data-grav-array-action]:not([data-grav-array-action="sort"])', (event) => this.actionEvent(event));
this.arrays = $();
$('[data-grav-field="array"]').each((index, list) => this.addArray(list));
$('body').on('mutation._grav', this._onAddedNodes.bind(this));
}
addArray(list) {
list = $(list);
list.find('[data-grav-array-type="container"]').each((index, container) => {
container = $(container);
if (container.data('array-sort') || container[0].hasAttribute('data-array-nosort')) { return; }
container.data('array-sort', new Sortable(container.get(0), {
handle: '.fa-bars',
animation: 150
}));
});
}
actionInput(event) {
let element = $(event.target);
let type = element.data('grav-array-type');
this._setTemplate(element);
let template = element.data('array-template');
let keyElement = type === 'key' ? element : element.siblings('[data-grav-array-type="key"]:first');
let valueElement = type === 'value' ? element : element.siblings('[data-grav-array-type="value"]:first');
let escaped_name = !template.isValueOnly() ? keyElement.val() : this.getIndexFor(element);
escaped_name = escaped_name.toString().replace(/\[/g, '%5B').replace(/]/g, '%5D');
let name = `${template.getName()}[${escaped_name}]`;
if (!template.isValueOnly() && (!keyElement.val() && !valueElement.val())) {
valueElement.attr('name', '');
} else {
// valueElement.attr('name', !valueElement.val() ? template.getName() : name);
valueElement.attr('name', name);
}
this.refreshNames(template);
}
actionEvent(event) {
event && event.preventDefault();
let element = $(event.target);
let action = element.data('grav-array-action');
let container = element.parents('[data-grav-array-type="container"]');
this._setTemplate(element);
this[`${action}Action`](element);
let siblings = container.find('> div');
container[siblings.length > 1 ? 'removeClass' : 'addClass']('one-child');
}
addAction(element) {
let template = element.data('array-template');
let row = element.closest('[data-grav-array-type="row"]');
row.after(template.getNewRow());
}
remAction(element) {
let template = element.data('array-template');
let row = element.closest('[data-grav-array-type="row"]');
let isLast = !row.siblings().length;
if (isLast) {
let newRow = $(template.getNewRow());
row.after(newRow);
newRow.find('[data-grav-array-type="value"]:last').attr('name', template.getName());
}
row.remove();
this.refreshNames(template);
}
refreshNames(template) {
if (!template.isValueOnly()) { return; }
let row = template.container.find('> div > [data-grav-array-type="row"]');
let inputs = row.find('[name]:not([name=""])');
inputs.each((index, input) => {
input = $(input);
let name = input.attr('name');
name = name.replace(/\[\d+\]$/, `[${index}]`);
input.attr('name', name);
});
if (!inputs.length) {
row.find('[data-grav-array-type="value"]').attr('name', template.getName());
}
}
getIndexFor(element) {
let template = element.data('array-template');
let row = element.closest('[data-grav-array-type="row"]');
return template.container.find(`${template.isValueOnly() ? '> div ' : ''} > [data-grav-array-type="row"]`).index(row);
}
_setTemplate(element) {
if (!element.data('array-template')) {
element.data('array-template', new Template(element.closest('[data-grav-array-name]')));
}
}
_onAddedNodes(event, target/* , record, instance */) {
let arrays = $(target).find('[data-grav-field="array"]');
if (!arrays.length) { return; }
arrays.each((index, list) => {
list = $(list);
if (!~this.arrays.index(list)) {
this.addArray(list);
}
});
}
}
export let Instance = new ArrayField();

View File

@@ -0,0 +1,340 @@
import $ from 'jquery';
import Dropzone from 'dropzone';
// import EXIF from 'exif-js';
import {config, translations} from 'grav-form';
// translations
const Dictionary = {
dictCancelUpload: translations.PLUGIN_FORM.DROPZONE_CANCEL_UPLOAD,
dictCancelUploadConfirmation: translations.PLUGIN_FORM.DROPZONE_CANCEL_UPLOAD_CONFIRMATION,
dictDefaultMessage: translations.PLUGIN_FORM.DROPZONE_DEFAULT_MESSAGE,
dictFallbackMessage: translations.PLUGIN_FORM.DROPZONE_FALLBACK_MESSAGE,
dictFallbackText: translations.PLUGIN_FORM.DROPZONE_FALLBACK_TEXT,
dictFileTooBig: translations.PLUGIN_FORM.DROPZONE_FILE_TOO_BIG,
dictInvalidFileType: translations.PLUGIN_FORM.DROPZONE_INVALID_FILE_TYPE,
dictMaxFilesExceeded: translations.PLUGIN_FORM.DROPZONE_MAX_FILES_EXCEEDED,
dictRemoveFile: translations.PLUGIN_FORM.DROPZONE_REMOVE_FILE,
dictRemoveFileConfirmation: translations.PLUGIN_FORM.DROPZONE_REMOVE_FILE_CONFIRMATION,
dictResponseError: translations.PLUGIN_FORM.DROPZONE_RESPONSE_ERROR
};
Dropzone.autoDiscover = false;
const DropzoneMediaConfig = {
createImageThumbnails: {thumbnailWidth: 150},
addRemoveLinks: false,
dictDefaultMessage: Dictionary.dictDefaultMessage,
dictRemoveFileConfirmation: Dictionary.dictRemoveFileConfirmation,
previewTemplate: ''
};
// window.EXIF = EXIF;
export default class FilesField {
constructor({container = '.dropzone.files-upload', options = {}} = {}) {
this.container = $(container);
if (!this.container.length) {
return;
}
this.urls = {};
DropzoneMediaConfig.previewTemplate = $('#dropzone-template').html();
this.options = Object.assign({}, Dictionary, DropzoneMediaConfig, {
klass: this,
url: this.container.data('file-url-add') || config.current_url,
acceptedFiles: this.container.data('media-types'),
init: this.initDropzone
}, this.container.data('dropzone-options'), options);
this.dropzone = new Dropzone(container, this.options);
this.dropzone.on('complete', this.onDropzoneComplete.bind(this));
this.dropzone.on('success', this.onDropzoneSuccess.bind(this));
this.dropzone.on('removedfile', this.onDropzoneRemovedFile.bind(this));
this.dropzone.on('sending', this.onDropzoneSending.bind(this));
this.dropzone.on('error', this.onDropzoneError.bind(this));
}
initDropzone() {
let files = this.options.klass.container.find('[data-file]');
let dropzone = this;
if (!files.length) {
return;
}
files.each((index, file) => {
file = $(file);
let data = file.data('file');
let mock = {
name: data.name,
size: data.size,
type: data.type,
status: Dropzone.ADDED,
accepted: true,
url: this.options.url,
removeUrl: data.remove,
data
};
dropzone.files.push(mock);
dropzone.options.addedfile.call(dropzone, mock);
if (mock.type.match(/^image\//)) dropzone.options.thumbnail.call(dropzone, mock, data.path);
file.remove();
});
}
getURI() {
return this.container.data('mediaUri') || '';
}
onDropzoneSending(file, xhr, formData) {
const form = this.container.closest('form');
const unique_id = form.find('[name="__unique_form_id__"]');
formData.append('__form-name__', form.find('[name="__form-name__"]').val());
if (unique_id.length) {
formData.append('__unique_form_id__', unique_id.val());
}
formData.append('__form-file-uploader__', 1);
formData.append('name', this.options.dotNotation);
formData.append('form-nonce', config.form_nonce);
formData.append('task', 'filesupload');
formData.append('uri', this.getURI());
}
onDropzoneSuccess(file, response, xhr) {
if (this.options.reloadPage) {
global.location.reload();
}
// store params for removing file from session before it gets saved
if (response.session) {
file.sessionParams = response.session;
file.removeUrl = this.options.url;
// Touch field value to force a mutation detection
const input = this.container.find('[name][type="hidden"]');
const value = input.val();
input.val(value + ' ');
}
return this.handleError({
file,
data: response,
mode: 'removeFile',
msg: `<p>${translations.PLUGIN_FORM.FILE_ERROR_UPLOAD} <strong>${file.name}</strong></p>
<pre>${response.message}</pre>`
});
}
onDropzoneComplete(file) {
if (!file.accepted && !file.rejected) {
let data = {
status: 'error',
message: `${translations.PLUGIN_FORM.FILE_UNSUPPORTED}: ${file.name.match(/\..+/).join('')}`
};
return this.handleError({
file,
data,
mode: 'removeFile',
msg: `<p>${translations.PLUGIN_FORM.FILE_ERROR_ADD} <strong>${file.name}</strong></p>
<pre>${data.message}</pre>`
});
}
if (this.options.reloadPage) {
global.location.reload();
}
}
onDropzoneRemovedFile(file, ...extra) {
if (!file.accepted || file.rejected) {
return;
}
const form = this.container.closest('form');
const unique_id = form.find('[name="__unique_form_id__"]');
let url = file.removeUrl || this.urls.delete || `${location.href}.json`;
let path = (url || '').match(/path:(.*)\//);
let data = new FormData();
data.append('filename', file.name);
data.append('__form-name__', form.find('[name="__form-name__"]').val());
data.append('name', this.options.dotNotation);
data.append('form-nonce', config.form_nonce);
data.append('uri', this.getURI());
if (file.sessionParams) {
data.append('__form-file-remover__', '1');
data.append('session', file.sessionParams);
}
if (unique_id.length) {
data.append('__unique_form_id__', unique_id.val());
}
$.ajax({
url,
data,
method: 'POST',
contentType: false,
processData: false,
success: () => {
if (!path) {
return;
}
path = global.atob(path[1]);
let input = this.container.find('[name][type="hidden"]');
let data = JSON.parse(input.val() || '{}');
delete data[path];
input.val(JSON.stringify(data));
}
});
}
onDropzoneError(file, response, xhr) {
let message = xhr && response.error ? response.error.message : response;
$(file.previewElement).find('[data-dz-errormessage]').html(message);
return this.handleError({
file,
data: {status: 'error'},
msg: `<pre>${message}</pre>`
});
}
handleError(options) {
return true;
/* let { file, data, mode, msg } = options;
if (data.status !== 'error' && data.status !== 'unauthorized') { return; }
switch (mode) {
case 'addBack':
if (file instanceof File) {
this.dropzone.addFile.call(this.dropzone, file);
} else {
this.dropzone.files.push(file);
this.dropzone.options.addedfile.call(this.dropzone, file);
this.dropzone.options.thumbnail.call(this.dropzone, file, file.extras.url);
}
break;
case 'removeFile':
default:
if (~this.dropzone.files.indexOf(file)) {
file.rejected = true;
this.dropzone.removeFile.call(this.dropzone, file, { silent: true });
}
break;
}
let modal = $('[data-remodal-id="generic"]');
modal.find('.error-content').html(msg);
$.remodal.lookup[modal.data('remodal')].open(); */
}
}
/*
export function UriToMarkdown(uri) {
uri = uri.replace(/@3x|@2x|@1x/, '');
uri = uri.replace(/\(/g, '%28');
uri = uri.replace(/\)/g, '%29');
return uri.match(/\.(jpe?g|png|gif|svg)$/i) ? `![](${uri})` : `[${decodeURI(uri)}](${uri})`;
}
*/
let instances = [];
let cache = $();
const onAddedNodes = (event, target/* , record, instance */) => {
let files = $(target).find('.dropzone.files-upload');
if (!files.length) {
return;
}
files.each((index, file) => {
file = $(file);
if (!~cache.index(file)) {
addNode(file);
}
});
};
const addNode = (container) => {
container = $(container);
let input = container.find('input[type="file"]');
let settings = container.data('grav-file-settings') || {};
if (settings.accept && ~settings.accept.indexOf('*')) {
settings.accept = [''];
}
let options = {
url: container.data('file-url-add') || (container.closest('form').attr('action') || config.current_url) + '.json',
paramName: settings.paramName || 'file',
dotNotation: settings.name || 'file',
acceptedFiles: settings.accept ? settings.accept.join(',') : input.attr('accept') || container.data('media-types'),
maxFilesize: settings.filesize || 256,
maxFiles: settings.limit || null,
resizeWidth: settings.resizeWidth || null,
resizeHeight: settings.resizeHeight || null,
resizeQuality: settings.resizeQuality || null,
accept: function(file, done) {
const resolution = settings.resolution;
let error = '';
if (!resolution) return done();
if ((this.options.maxFiles != null) && (this.getAcceptedFiles().length >= this.options.maxFiles)) {
done(this.options.dictMaxFilesExceeded.replace('{{maxFiles}}', this.options.maxFiles));
return this.emit('maxfilesexceeded', file);
}
const reader = new FileReader();
if (resolution.min || (!(settings.resizeWidth || settings.resizeHeight) && resolution.max)) {
reader.onload = function(event) {
const image = new Image();
image.src = event.target.result;
image.onload = function() {
if (resolution.min) {
Object.keys(resolution.min).forEach((attr) => {
if (this[attr] < resolution.min[attr]) {
error += translations.PLUGIN_FORM.RESOLUTION_MIN.replace(/{{attr}}/g, attr).replace(/{{min}}/g, resolution.min[attr]);
}
});
}
if (!(settings.resizeWidth || settings.resizeHeight)) {
if (resolution.max) {
Object.keys(resolution.max).forEach((attr) => {
if (this[attr] > resolution.max[attr]) {
error += translations.PLUGIN_FORM.RESOLUTION_MAX.replace(/{{attr}}/g, attr).replace(/{{max}}/g, resolution.max[attr]);
}
});
}
}
done(error);
};
};
reader.readAsDataURL(file);
} else {
return done(error);
}
}
};
cache = cache.add(container);
container = container[0];
instances.push(new FilesField({container, options}));
};
export let Instances = (() => {
$(document).ready(() => {
$('.dropzone.files-upload').each((i, container) => addNode(container));
$('body').on('mutation._grav', onAddedNodes);
});
return instances;
})();

View File

@@ -0,0 +1,94 @@
import $ from 'jquery';
const attachToggleables = (form) => {
form = $(form);
let query = '[data-grav-field="toggleable"] input[type="checkbox"]';
form.on('change', query, (event) => {
let toggle = $(event.target);
let enabled = toggle.is(':checked');
let parent = toggle.closest('.form-field');
let label = parent.find('label.toggleable');
let fields = parent.find('.form-data');
let inputs = fields.find('input, select, textarea, button');
label.add(fields).css('opacity', enabled ? '' : 0.7);
inputs.map((index, input) => {
let isSelectize = input.selectize;
input = $(input);
if (isSelectize) {
isSelectize[enabled ? 'enable' : 'disable']();
} else {
input.prop('disabled', !enabled);
}
});
});
form.find(query).trigger('change');
};
const attachDisabledFields = (form) => {
form = $(form);
let prefix = '.form-field-toggleable .form-data';
let query = [];
['input', 'select', 'label[for]', 'textarea', '.selectize-control'].forEach((item) => {
query.push(`${prefix} ${item}`);
});
form.on('mousedown', query.join(', '), (event) => {
let input = $(event.target);
let isFor = input.prop('for');
let isSelectize = (input.hasClass('selectize-control') || input.parents('.selectize-control')).length;
if (isFor) { input = $(`[id="${isFor}"]`); }
if (isSelectize) { input = input.closest('.selectize-control').siblings('select[name]'); }
if (!input.prop('disabled')) { return true; }
let toggle = input.closest('.form-field').find('[data-grav-field="toggleable"] input[type="checkbox"]');
toggle.trigger('click');
});
};
/*
const submitUncheckedFields = (forms) => {
forms = $(forms);
let submitted = false;
forms.each((index, form) => {
form = $(form);
form.on('submit', () => {
// workaround for MS Edge, submitting multiple forms at the same time
if (submitted) { return false; }
let formId = form.attr('id');
let unchecked = form.find('input[type="checkbox"]:not(:checked):not(:disabled)');
let submit = form.find('[type="submit"]').add(`[form="${formId}"][type="submit"]`);
if (!unchecked.length) { return true; }
submit.addClass('pointer-events-disabled');
unchecked.each((index, element) => {
element = $(element);
let name = element.prop('name');
let fake = $(`<input type="hidden" name="${name}" value="0" />`);
form.append(fake);
});
submitted = true;
return true;
});
});
};
*/
$(document).ready(() => {
const forms = $('form').filter((form) => $(form).find('[name="__form-name__"]'));
if (!forms.length) { return; }
forms.each((index, form) => {
attachToggleables(form);
attachDisabledFields(form);
// submitUncheckedFields(form);
});
});

View File

@@ -0,0 +1,7 @@
import './form';
import FileInstances from './file';
import ArrayInstances from './array';
import PageMedia, { Instance as PageMediaInstances } from './media';
import './tabs';
export default { FileInstances, ArrayInstances, Media: { PageMedia, PageMediaInstances } };

View File

@@ -0,0 +1,174 @@
import $ from 'jquery';
import FilesField from './file';
import { config, translations } from 'grav-form';
import Sortable from 'sortablejs';
const template = `
<div class="dz-preview dz-file-preview">
<div class="dz-details">
<div class="dz-filename"><span data-dz-name></span></div>
<div class="dz-size" data-dz-size></div>
<img data-dz-thumbnail />
</div>
<div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>
<div class="dz-success-mark"><span>✔</span></div>
<div class="dz-error-mark"><span>✘</span></div>
<div class="dz-error-message"><span data-dz-errormessage></span></div>
<a class="dz-remove" title="${translations.PLUGIN_FORM.DELETE}" href="javascript:undefined;" data-dz-remove>${translations.PLUGIN_FORM.DELETE}</a>
</div>`.trim();
export default class PageMedia extends FilesField {
constructor({ container = '#grav-dropzone', options = {} } = {}) {
const previewTemplate = $('#dropzone-media-template').html() || template;
options = Object.assign(options, { previewTemplate });
super({ container, options });
if (!this.container.length) { return; }
this.urls = {
fetch: `${this.container.data('media-url')}/task${config.param_sep}listmedia`,
add: `${this.container.data('media-url')}/task${config.param_sep}addmedia`,
delete: `${this.container.data('media-url')}/task${config.param_sep}delmedia`
};
this.dropzone.options.url = this.urls.add;
if (typeof this.options.fetchMedia === 'undefined' || this.options.fetchMedia) {
this.fetchMedia();
}
const field = $(`[name="${this.container.data('dropzone-field')}"]`);
if (field.length) {
this.sortable = new Sortable(this.container.get(0), {
animation: 150,
// forceFallback: true,
setData: (dataTransfer, target) => {
target = $(target);
this.dropzone.disable();
target.addClass('hide-backface');
dataTransfer.effectAllowed = 'copy';
},
onSort: () => {
let names = [];
this.container.find('[data-dz-name]').each((index, file) => {
file = $(file);
const name = file.text().trim();
names.push(name);
});
field.val(names.join(','));
}
});
}
}
onDropzoneRemovedFile(file, ...extra) {
if (!file.accepted || file.rejected) { return; }
const form = this.container.closest('form');
const unique_id = form.find('[name="__unique_form_id__"]');
let url = file.removeUrl || this.urls.delete || `${location.href}.json`;
let path = (url || '').match(/path:(.*)\//);
let data = new FormData();
data.append('filename', file.name);
data.append('__form-name__', form.find('[name="__form-name__"]').val());
if (unique_id.length) {
data.append('__unique_form_id__', unique_id.val());
}
data.append('name', this.options.dotNotation);
data.append('form-nonce', config.form_nonce);
if (file.sessionParams) {
data.append('__form-file-remover__', '1');
data.append('session', file.sessionParams);
}
$.ajax({
url,
data,
method: 'POST',
contentType: false,
processData: false,
success: () => {
if (!path) { return; }
path = global.atob(path[1]);
let input = this.container.find('[name][type="hidden"]');
let data = JSON.parse(input.val() || '{}');
delete data[path];
input.val(JSON.stringify(data));
}
});
}
fetchMedia() {
const order = this.container.closest('.form-field').find('[name="data[header][media_order]"]').val();
const data = { order };
let url = this.urls.fetch;
$.ajax({
url,
method: 'POST',
data,
success: (response) => {
if (typeof response === 'string' || response instanceof String) {
return false;
}
response = response.results;
Object.keys(response).forEach((name) => {
let data = response[name];
let mock = { name, size: data.size, accepted: true, extras: data };
this.dropzone.files.push(mock);
this.dropzone.options.addedfile.call(this.dropzone, mock);
this.dropzone.options.thumbnail.call(this.dropzone, mock, data.url);
});
this.container.find('.dz-preview').prop('draggable', 'true');
}
});
/*
request(url, { method: 'post', body }, (response) => {
let results = response.results;
Object.keys(results).forEach((name) => {
let data = results[name];
let mock = { name, size: data.size, accepted: true, extras: data };
this.dropzone.files.push(mock);
this.dropzone.options.addedfile.call(this.dropzone, mock);
this.dropzone.options.thumbnail.call(this.dropzone, mock, data.url);
});
this.container.find('.dz-preview').prop('draggable', 'true');
});*/
}
onDropzoneSending(file, xhr, formData) {
/*
// Cannot call super because Safari and IE API don't implement `delete`
super.onDropzoneSending(file, xhr, formData);
formData.delete('task');
*/
formData.append('name', this.options.dotNotation);
formData.append('admin-nonce', config.admin_nonce);
}
onDropzoneComplete(file) {
super.onDropzoneComplete(file);
this.sortable.options.onSort();
// accepted
$('.dz-preview').prop('draggable', 'true');
}
// onDropzoneRemovedFile(file, ...extra) {
// super.onDropzoneRemovedFile(file, ...extra);
// this.sortable.options.onSort();
// }
}
export let Instance = new PageMedia();

View File

@@ -0,0 +1,14 @@
import $ from 'jquery';
$('body').on('touchstart click', '[data-tabid]', (event) => {
event && event.stopPropagation();
let target = $(event.currentTarget);
const panel = $(`[id="${target.data('tabid')}"]`);
target.siblings('[data-tabid]').removeClass('active');
target.addClass('active');
panel.siblings('[id]').removeClass('active');
panel.addClass('active');
});

View File

@@ -0,0 +1,4 @@
import Instances from './fields';
import './utils/keep-alive';
export { Instances };

View File

@@ -0,0 +1,17 @@
import $ from 'jquery';
import {config} from 'grav-form';
const MAX_SAFE_DELAY = 2147483647;
$(document).ready(() => {
const keepAlive = $('[data-grav-keepalive]');
if (keepAlive.length) {
const timeout = config.session_timeout / 1.5 * 1000;
setInterval(() => {
$.ajax({
url: `${config.base_url_relative}/task${config.param_sep}keep-alive`
});
}, Math.min(timeout, MAX_SAFE_DELAY));
}
});