init commit
This commit is contained in:
463
ui/src/core/file-upload.js
Normal file
463
ui/src/core/file-upload.js
Normal file
@@ -0,0 +1,463 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
let uploadTemplate = null;
|
||||
let videoImageCovers = {};
|
||||
|
||||
/**
|
||||
* Opens the upload form
|
||||
* @param options
|
||||
*/
|
||||
window.openUploadForm = function(options) {
|
||||
options = $.extend(true, {}, {
|
||||
templateId: 'template-file-upload',
|
||||
uploadTemplateId: 'template-upload',
|
||||
downloadTemplateId: 'template-download',
|
||||
videoImageCovers: true,
|
||||
className: '',
|
||||
animateDialog: true,
|
||||
formOpenedEvent: null,
|
||||
onHideCallback: null,
|
||||
templateOptions: {
|
||||
layoutImport: false,
|
||||
multi: true,
|
||||
includeTagsInput: true,
|
||||
},
|
||||
}, options);
|
||||
|
||||
// Keep a cache of the upload template (unless we are a non-standard form)
|
||||
if (
|
||||
uploadTemplate === null ||
|
||||
options.templateId !== 'template-file-upload'
|
||||
) {
|
||||
uploadTemplate = Handlebars.compile($('#' + options.templateId).html());
|
||||
}
|
||||
|
||||
if (typeof maxImagePixelSize === undefined || maxImagePixelSize === '') {
|
||||
maxImagePixelSize = 0;
|
||||
}
|
||||
|
||||
// Handle bars and open a dialog
|
||||
const dialog = bootbox.dialog({
|
||||
message: uploadTemplate(options.templateOptions),
|
||||
title: options.title,
|
||||
buttons: options.buttons,
|
||||
className: options.className + ' upload-modal',
|
||||
animate: options.animateDialog,
|
||||
size: 'large',
|
||||
}).on('hidden.bs.modal', function() {
|
||||
// Reset video image covers.
|
||||
videoImageCovers = {};
|
||||
|
||||
// Call the onHideCallback if it exists
|
||||
if (options.onHideCallback !== null) {
|
||||
options.onHideCallback(dialog);
|
||||
}
|
||||
}).attr('id', Date.now());
|
||||
|
||||
setTimeout(function() {
|
||||
console.debug('Timeout fired, we should be shown by now');
|
||||
|
||||
// Configure the upload form
|
||||
const form = $(dialog).find('form');
|
||||
let uploadOptions = {
|
||||
url: options.url,
|
||||
disableImageResize: true,
|
||||
previewMaxWidth: 100,
|
||||
previewMaxHeight: 100,
|
||||
previewCrop: true,
|
||||
acceptFileTypes: new RegExp(
|
||||
'\\.(' + options.templateOptions.upload.validExt + ')$',
|
||||
'i',
|
||||
),
|
||||
maxFileSize: options.templateOptions.upload.maxSize,
|
||||
includeTagsInput: options.templateOptions.includeTagsInput,
|
||||
uploadTemplateId: options.uploadTemplateId,
|
||||
limitConcurrentUploads: 3,
|
||||
};
|
||||
let refreshSessionInterval;
|
||||
|
||||
$(form).on('keydown', function(event) {
|
||||
if (event.keyCode == 13) {
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Video thumbnail capture.
|
||||
if (options.videoImageCovers) {
|
||||
$(dialog).find('#files').on('change', handleVideoCoverImage);
|
||||
}
|
||||
|
||||
if (maxImagePixelSize > 0) {
|
||||
$(dialog).find('#files').on('change', checkImagePixelSize);
|
||||
}
|
||||
|
||||
// If we are not a multi-upload, then limit to 1
|
||||
if (!options.templateOptions.multi) {
|
||||
uploadOptions = $.extend({}, uploadOptions, {
|
||||
maxNumberOfFiles: 1,
|
||||
limitMultiFileUploads: 1,
|
||||
});
|
||||
}
|
||||
|
||||
// Widget dates?
|
||||
if (options.templateOptions.showWidgetDates) {
|
||||
XiboInitialise('.row-widget-dates');
|
||||
}
|
||||
|
||||
// Handle expiry dates fields
|
||||
const expiryDatesStatus = function() {
|
||||
const setExpiryFlag = form.find('#setExpiryDates').is(':checked');
|
||||
|
||||
// Hide and disable fiels ( to avoid form submitting)
|
||||
form.find('.row-widget-set-expiry').toggleClass('hidden', !setExpiryFlag);
|
||||
form.find('.row-widget-set-expiry input')
|
||||
.prop('disabled', !setExpiryFlag);
|
||||
};
|
||||
|
||||
// Call when checkbox changes
|
||||
form.find('#setExpiryDates').on('change', expiryDatesStatus);
|
||||
|
||||
// Call on start
|
||||
expiryDatesStatus();
|
||||
|
||||
// Ready to initialise the widget and bind to some events
|
||||
form
|
||||
.fileupload(uploadOptions)
|
||||
.bind('fileuploadsubmit', function(e, data) {
|
||||
const inputs = data.context.find(':input');
|
||||
if (inputs.filter('[required][value=""]').first().focus().length) {
|
||||
return false;
|
||||
}
|
||||
data.formData = inputs.serializeArray().concat(form.serializeArray());
|
||||
|
||||
inputs.filter('input').prop('disabled', true);
|
||||
})
|
||||
.bind('fileuploadstart', function(e, data) {
|
||||
// Show progress data
|
||||
form.find('.fileupload-progress .progress-extended').show();
|
||||
form.find('.fileupload-progress .progress-end').hide();
|
||||
|
||||
if (form.fileupload('active') <= 0) {
|
||||
refreshSessionInterval =
|
||||
setInterval(
|
||||
'XiboPing(\'' + pingUrl + '?refreshSession=true\')',
|
||||
1000 * 60 * 3,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.bind('fileuploaddone', function(e, data) {
|
||||
// if we throw an error in the backend, the
|
||||
// data.result.files is undefined, check if we have a message
|
||||
if (
|
||||
data.result.files === undefined &&
|
||||
data.result.message !== undefined &&
|
||||
data.result.message != null
|
||||
) {
|
||||
toastr.error(data.result.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the upload was an error, then don't process the remaining methods.
|
||||
if (
|
||||
data.result.files[0].error != null &&
|
||||
data.result.files[0].error !== ''
|
||||
) {
|
||||
toastr.error(data.result.files[0].error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.videoImageCovers) {
|
||||
saveVideoCoverImage(data);
|
||||
}
|
||||
|
||||
if (refreshSessionInterval != null && form.fileupload('active') <= 0) {
|
||||
clearInterval(refreshSessionInterval);
|
||||
}
|
||||
|
||||
// Run the callback function for done when
|
||||
// we're processing the last uploading element
|
||||
const filesToUploadCount = form.find('tr.template-upload').length;
|
||||
if (
|
||||
filesToUploadCount == 1 &&
|
||||
options.uploadDoneEvent !== undefined &&
|
||||
options.uploadDoneEvent !== null &&
|
||||
typeof options.uploadDoneEvent == 'function'
|
||||
) {
|
||||
// Run in a short while.
|
||||
// this gives time for file-upload's own deferreds to run
|
||||
setTimeout(function() {
|
||||
options.uploadDoneEvent(data);
|
||||
}, 300);
|
||||
}
|
||||
})
|
||||
.bind('fileuploadprogressall', function(e, data) {
|
||||
// Hide progress data and show processing
|
||||
if (data.total > 0 && data.loaded === data.total) {
|
||||
form.find('.fileupload-progress .progress-extended').hide();
|
||||
form.find('.fileupload-progress .progress-end').show();
|
||||
}
|
||||
})
|
||||
.bind('fileuploadadd', function(e, data) {
|
||||
if (uploadOptions.limitMultiFileUploads === 1) {
|
||||
const totalFiles =
|
||||
form.find('.files > tr.template-upload, tr.template-download')
|
||||
.length;
|
||||
|
||||
// Check if the number of files exceeds the limit
|
||||
if (totalFiles >= uploadOptions.maxNumberOfFiles) {
|
||||
// Show toast error message
|
||||
toastr.error(
|
||||
translations.canOnlyUploadMax
|
||||
.replace('%s', uploadOptions.maxNumberOfFiles),
|
||||
);
|
||||
|
||||
// Prevent adding the file
|
||||
return false;
|
||||
}
|
||||
}
|
||||
})
|
||||
.bind('fileuploadadded fileuploadcompleted fileuploadfinished',
|
||||
function(e, data) {
|
||||
// Get uploaded and downloaded files and toggle Done button
|
||||
const filesToUploadCount = form.find('tr.template-upload').length;
|
||||
const $button =
|
||||
form.parents('.modal:first').find('button.btn-bb-main');
|
||||
if (!options.templateOptions.includeTagsInput) {
|
||||
$('.tags-input-container').addClass('d-none');
|
||||
}
|
||||
if (filesToUploadCount === 0) {
|
||||
$button.removeAttr('disabled');
|
||||
videoImageCovers = {};
|
||||
} else {
|
||||
$button.attr('disabled', 'disabled');
|
||||
}
|
||||
})
|
||||
.bind('fileuploaddrop', function(e, data) {
|
||||
if (options.videoImageCovers) {
|
||||
handleVideoCoverImage(e, data);
|
||||
}
|
||||
|
||||
if (maxImagePixelSize > 0) {
|
||||
checkImagePixelSize(e, data);
|
||||
}
|
||||
});
|
||||
|
||||
if (options.templateOptions.folderSelector) {
|
||||
// Handle creating a folder selector
|
||||
// compile tree folder modal and append it to Form
|
||||
|
||||
// make bootstrap happy.
|
||||
if ($('#folder-tree-form-modal').length != 0) {
|
||||
$('#folder-tree-form-modal').remove();
|
||||
}
|
||||
|
||||
if ($('#folder-tree-form-modal').length === 0) {
|
||||
const folderTreeModal = templates['folder-tree'];
|
||||
$('body').append(folderTreeModal({
|
||||
container: 'container-folder-form-tree',
|
||||
modal: 'folder-tree-form-modal',
|
||||
trans: translations.folderTree,
|
||||
}));
|
||||
|
||||
$('#folder-tree-form-modal').on('hidden.bs.modal', function(ev) {
|
||||
// Fix for 2nd/overlay modal
|
||||
$('.modal:visible').length && $(document.body).addClass('modal-open');
|
||||
|
||||
$(ev.currentTarget).data('bs.modal', null);
|
||||
});
|
||||
}
|
||||
|
||||
// Init JS Tree
|
||||
initJsTreeAjax(
|
||||
$('#folder-tree-form-modal').find('#container-folder-form-tree'),
|
||||
options.initialisedBy,
|
||||
true,
|
||||
600,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle any form opened event
|
||||
if (
|
||||
options.formOpenedEvent !== null &&
|
||||
options.formOpenedEvent !== undefined
|
||||
) {
|
||||
eval(options.formOpenedEvent)(dialog);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
return dialog;
|
||||
};
|
||||
|
||||
/**
|
||||
* Binds to a File Input and listens for changes
|
||||
* when it finds some it sets up for capturing a video thumbnail
|
||||
* @param e
|
||||
* @param data
|
||||
*/
|
||||
function handleVideoCoverImage(e, data) {
|
||||
// handle click and drag&drop ways
|
||||
const files = data === undefined ? e.currentTarget.files : data.files;
|
||||
let video = null;
|
||||
|
||||
// wait a little bit for the preview to be in the form
|
||||
const checkExist = setInterval(function() {
|
||||
if ($('.preview').find('video').length) {
|
||||
let allVideoPreviewsExist = true;
|
||||
|
||||
// iterate through our files, check if we have videos
|
||||
// if we do, then set params on video object,
|
||||
// convert 2nd second of the video to an image
|
||||
// and register onseeked and onpause events
|
||||
Array.from(files).forEach(file => {
|
||||
if (file.error || !file.type.includes('video')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file.preview) {
|
||||
allVideoPreviewsExist = false;
|
||||
return;
|
||||
}
|
||||
|
||||
video = file.preview;
|
||||
video.name = file.name;
|
||||
video.setAttribute('id', file.name);
|
||||
video.preload = 'metadata';
|
||||
video.onseeked = createImage;
|
||||
video.onpause = createImage;
|
||||
// set current time to trigger event
|
||||
// and create the cover image
|
||||
video.currentTime = 2;
|
||||
});
|
||||
|
||||
// Clear interval when all previews exist
|
||||
if (allVideoPreviewsExist) {
|
||||
// show help text describing this feature.
|
||||
const helpText = translations.videoImageCoverHelpText;
|
||||
const $helpTextSelector = $('.template-upload video:first')
|
||||
.closest('tr')
|
||||
.find('td span.info');
|
||||
$helpTextSelector.empty();
|
||||
$helpTextSelector.append(helpText);
|
||||
|
||||
clearInterval(checkExist);
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function createImage() {
|
||||
// eslint-disable-next-line no-invalid-this
|
||||
const self = this;
|
||||
// this will actually create the image
|
||||
// and save it to an object with file name as a key
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = self.videoHeight;
|
||||
canvas.width = self.videoWidth;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(self, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
const videoImageCover = new Image();
|
||||
videoImageCover.src = canvas.toDataURL();
|
||||
|
||||
videoImageCovers[self.name] = videoImageCover.src;
|
||||
}
|
||||
|
||||
function saveVideoCoverImage(data) {
|
||||
// this is called when fileUpload is finished
|
||||
// reason being that we need mediaId to save videoCover image correctly.
|
||||
const results = data.result.files[0];
|
||||
const thumbnailData = {};
|
||||
|
||||
// we only want to call this for videos
|
||||
// (it would not do anything for other types).
|
||||
if (results.mediaType === 'video') {
|
||||
// get mediaId from results (finished upload)
|
||||
thumbnailData['mediaId'] = results.mediaId;
|
||||
|
||||
// get the base64 image we captured and stored for this file name
|
||||
thumbnailData['image'] = videoImageCovers[results.fileName];
|
||||
|
||||
// remove this key from our object
|
||||
delete videoImageCovers[results.name];
|
||||
|
||||
// this calls function in library controller that decodes the image and
|
||||
// saves it to library as
|
||||
// "{libraryLocation}/{$mediaId}_{mediaType}cover.png".
|
||||
$.ajax({
|
||||
url: addMediaThumbnailUrl,
|
||||
type: 'POST',
|
||||
data: thumbnailData,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Binds to a File Input and listens for changes,
|
||||
* if Image was added, check the max resize limit
|
||||
* and show a warning message if added image is too large
|
||||
* @param e
|
||||
* @param data
|
||||
*/
|
||||
function checkImagePixelSize(e, data) {
|
||||
const files = data === undefined ? e.currentTarget.files : data.files;
|
||||
|
||||
const $existingFiles = $('.template-upload canvas')
|
||||
.closest('tr')
|
||||
.find('td span.info');
|
||||
|
||||
const checkExist = setInterval(function() {
|
||||
if ($('.preview').find('canvas').length) {
|
||||
// iterate through our files
|
||||
Array.from(files).forEach(function(file, index) {
|
||||
if (!file.error && file.type.includes('image')) {
|
||||
// if we have existing files, adjust index
|
||||
// to ensure we put the warning in the right place
|
||||
if ($existingFiles.length > 0) {
|
||||
if (index === 0) {
|
||||
index = $existingFiles.length;
|
||||
} else {
|
||||
index += $existingFiles.length;
|
||||
}
|
||||
}
|
||||
img = new Image();
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
img.onload = function() {
|
||||
if (this.width > maxImagePixelSize ||
|
||||
this.height > maxImagePixelSize
|
||||
) {
|
||||
const helpText = translations.imagePixelSizeTooLarge;
|
||||
$('.template-upload canvas')
|
||||
.closest('tr')
|
||||
.find('td span.info')[index]
|
||||
.append(helpText);
|
||||
}
|
||||
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
};
|
||||
img.src = objectUrl;
|
||||
}
|
||||
});
|
||||
|
||||
clearInterval(checkExist);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
5237
ui/src/core/forms.js
Normal file
5237
ui/src/core/forms.js
Normal file
File diff suppressed because it is too large
Load Diff
453
ui/src/core/help-pane.js
Normal file
453
ui/src/core/help-pane.js
Normal file
@@ -0,0 +1,453 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Xibo Signage Ltd
|
||||
*
|
||||
* Xibo - Digital Signage - https://xibosignage.com
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
$(function() {
|
||||
const $help = $('#help-pane');
|
||||
const $helpButton = $help.find('.help-pane-btn');
|
||||
const $helpContainer = $help.find('.help-pane-container');
|
||||
let fileStore = [];
|
||||
|
||||
// 0: Disabled, 1: Main, 2: Feedback form, 3: Feedback outro
|
||||
let helperStep = 0;
|
||||
|
||||
const hideHelper = function() {
|
||||
$helpContainer.hide();
|
||||
$('.help-pane-overlay').remove();
|
||||
};
|
||||
|
||||
const renderPanelContent = function() {
|
||||
if (helperStep === 2) {
|
||||
// Feedback form
|
||||
$helpContainer.html(
|
||||
templates.help.feedbackForm({
|
||||
trans: translations.helpPane,
|
||||
pageURL: window.location.pathname,
|
||||
faultViewUrl: $help.data('faultViewUrl'),
|
||||
faultViewEnabled: $help.data('faultViewEnabled') == 1,
|
||||
accountId: accountId,
|
||||
currentUserName,
|
||||
currentUserEmail,
|
||||
}),
|
||||
);
|
||||
|
||||
// Privacy info popover
|
||||
$helpContainer.find('.help-pane-feedback-privacy-info > i')
|
||||
.popover({
|
||||
container: '.help-pane-container',
|
||||
placement: 'top',
|
||||
delay: {
|
||||
show: 200,
|
||||
hide: 50,
|
||||
},
|
||||
trigger: 'hover',
|
||||
});
|
||||
|
||||
handleFileUpload();
|
||||
} else {
|
||||
// Main or end panel
|
||||
const template = (helperStep === 3) ?
|
||||
templates.help.endPanel :
|
||||
templates.help.mainPanel;
|
||||
|
||||
$helpContainer.html(
|
||||
template(
|
||||
{
|
||||
trans: translations.helpPane,
|
||||
helpLinks: $('#help-pane').data('helpLinks'),
|
||||
helpLandingPageURL: $('#help-pane').data('urlHelpLandingPage'),
|
||||
isXiboThemed,
|
||||
welcomeViewURL,
|
||||
supportURL,
|
||||
appName,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
handleControls();
|
||||
};
|
||||
|
||||
const handleControls = function() {
|
||||
// Close button
|
||||
$help.find('.close-icon').on('click', hideHelper);
|
||||
|
||||
// Back button
|
||||
$help.find('.back-icon')
|
||||
.on('click', (ev) => {
|
||||
ev.preventDefault();
|
||||
// Move to previous screen
|
||||
helperStep--;
|
||||
renderPanelContent();
|
||||
});
|
||||
|
||||
// Feedback card button
|
||||
$help.find('.help-pane-card[data-action="feedback_form"]')
|
||||
.on('click', (ev) => {
|
||||
ev.preventDefault();
|
||||
helperStep = 2;
|
||||
renderPanelContent();
|
||||
});
|
||||
|
||||
// Submit form
|
||||
$help.find('.submit-form-btn')
|
||||
.on('click', (ev) => {
|
||||
ev.preventDefault();
|
||||
|
||||
// Show loading
|
||||
$helpContainer.append(
|
||||
$(`<div class="help-pane-loader">
|
||||
<i class="fas fa-spin fa-spinner"></i>
|
||||
</div>`),
|
||||
);
|
||||
|
||||
const $form = $help.find('form');
|
||||
const formData = new FormData($form[0]);
|
||||
|
||||
const validateEmail = function(email) {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
};
|
||||
|
||||
const showErrorOnInput = function($input, msg) {
|
||||
$input.parent('.xibo-form-input').addClass('invalid');
|
||||
$input.after($(`<div class="error-message">${msg}</div>`));
|
||||
};
|
||||
|
||||
// Remove invalid class from fields
|
||||
$form.find('.xibo-form-input.invalid')
|
||||
.removeClass('invalid');
|
||||
$form.find('.error-message').remove();
|
||||
$form.find('.feedback-form-error').addClass('d-none');
|
||||
|
||||
// Validate fields
|
||||
let isValid = true;
|
||||
|
||||
// User name
|
||||
const $userName = $form.find('[name=userName]');
|
||||
if (!$userName.val().trim()) {
|
||||
isValid = false;
|
||||
showErrorOnInput($userName, translations.helpPane.form.errors.name);
|
||||
}
|
||||
|
||||
// Email
|
||||
const $email = $form.find('[name=email]');
|
||||
const emailVal = $email.val().trim();
|
||||
if (!emailVal || !validateEmail(emailVal)) {
|
||||
isValid = false;
|
||||
showErrorOnInput($email, translations.helpPane.form.errors.email);
|
||||
}
|
||||
|
||||
// Message
|
||||
const $message = $form.find('[name=message]');
|
||||
if (!$message.val().trim()) {
|
||||
isValid = false;
|
||||
showErrorOnInput(
|
||||
$message,
|
||||
translations.helpPane.form.errors.comments,
|
||||
);
|
||||
}
|
||||
|
||||
// If any fields are invalid, show form error message
|
||||
if (!isValid) {
|
||||
// Hide loading
|
||||
$helpContainer.find('.help-pane-loader').remove();
|
||||
|
||||
// Show error
|
||||
$form.find('.feedback-form-error span')
|
||||
.html(translations.helpPane.form.errors.form);
|
||||
$form.find('.feedback-form-error')
|
||||
.removeClass('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate 32 char string as id
|
||||
const rndString =
|
||||
[...Array(32)].map(
|
||||
() => (Math.random() * 36 | 0).toString(36),
|
||||
).join('');
|
||||
formData.append('id', rndString);
|
||||
|
||||
fileStore.forEach((file) => {
|
||||
formData.append('files[]', file);
|
||||
});
|
||||
|
||||
// Submit form
|
||||
const requestOptions = {
|
||||
method: $form.data('method'),
|
||||
body: formData,
|
||||
};
|
||||
fetch($form.data('action'), requestOptions)
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
throw res;
|
||||
}
|
||||
|
||||
if (res.status === 204) {
|
||||
// Nothing more to do
|
||||
return;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((_res) => {
|
||||
// Clear file store
|
||||
fileStore = [];
|
||||
|
||||
// Hide loading
|
||||
$helpContainer.find('.help-pane-loader').remove();
|
||||
|
||||
// Sucess, go to final screen
|
||||
helperStep = 3;
|
||||
renderPanelContent();
|
||||
})
|
||||
.catch(async (error) => {
|
||||
let message = translations.helpPane.form.errors.request;
|
||||
try {
|
||||
const data = await error.json();
|
||||
message = data.message || message;
|
||||
} catch {
|
||||
try {
|
||||
const text = await error.text();
|
||||
if (text) {
|
||||
message = text;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Hide loading
|
||||
$helpContainer.find('.help-pane-loader').remove();
|
||||
|
||||
$form.find('.feedback-form-error span').html(message);
|
||||
$form.find('.feedback-form-error').removeClass('d-none');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleFileUpload = function() {
|
||||
// Attachments
|
||||
const $uploadMain = $help.find('.file-uploader-attachments');
|
||||
const $uploadsArea = $help.find('.uploads-area');
|
||||
const $uploadsDrop = $help.find('.uploads-drop');
|
||||
const $browseLink = $help.find('.upload-text-browse');
|
||||
const $fileInput = $help.find('#feedback_form_attachments');
|
||||
const $uploadedFiles = $help.find('.help-pane-upload-files');
|
||||
const maxFiles = 3;
|
||||
const maxFileSize = 15 * 1024 * 1024;
|
||||
const allowedTypes = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'application/pdf',
|
||||
'video/quicktime',
|
||||
];
|
||||
|
||||
// Show error message
|
||||
const showFileErrorMessage = function() {
|
||||
$uploadMain.append(templates.help.components.errorMessage({
|
||||
trans: translations.helpPane,
|
||||
}));
|
||||
|
||||
$uploadsArea.hide();
|
||||
};
|
||||
|
||||
const removeFileErrorMessage = function() {
|
||||
$uploadMain.find('.max-uploads-message').remove();
|
||||
$uploadsArea.show();
|
||||
};
|
||||
|
||||
// Add uploaded file to form
|
||||
const addFileCard = function(file) {
|
||||
$uploadedFiles.append(templates.help.components.uploadCard({
|
||||
name: file.name,
|
||||
type: file.fileTypeName,
|
||||
thumbURL: file.thumbURL,
|
||||
icon: file.fileIcon,
|
||||
}));
|
||||
|
||||
|
||||
// Update file container if we reach max files
|
||||
if ($uploadedFiles.find('.help-pane-upload-file').length >= maxFiles) {
|
||||
showFileErrorMessage();
|
||||
}
|
||||
|
||||
// Show file container
|
||||
$uploadedFiles.removeClass('d-none');
|
||||
};
|
||||
|
||||
const handleFilesDrop = function(files) {
|
||||
if (!files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentUploads =
|
||||
$uploadedFiles.find('.help-pane-upload-file').length;
|
||||
|
||||
if (currentUploads + files.length > maxFiles) {
|
||||
alert(translations.helpPane.form.errors.maxFiles);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
alert(
|
||||
file.name + ': ' +
|
||||
translations.helpPane.form.errors.invalidFileType);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.size > maxFileSize) {
|
||||
alert(
|
||||
file.name + ': ' +
|
||||
translations.helpPane.form.errors.fileTooLarge);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prevent duplicates
|
||||
if (fileStore.find(
|
||||
(f) => (f.name === file.name && f.size === file.size))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add to file store
|
||||
fileStore.push(file);
|
||||
|
||||
// Render files
|
||||
if (file.type.startsWith('image/')) {
|
||||
// Get thumb for image
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const thumbURL = e.target.result;
|
||||
addFileCard({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
thumbURL: thumbURL,
|
||||
fileTypeName: translations.helpPane.form.image,
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
// Get icons for others
|
||||
if (file.type === 'application/pdf') {
|
||||
file.fileIcon = 'fa-file-pdf';
|
||||
file.fileTypeName = translations.helpPane.form.pdf;
|
||||
} else if (file.type.startsWith('video/')) {
|
||||
file.fileIcon = 'fa-file-video';
|
||||
file.fileTypeName = translations.helpPane.form.video;
|
||||
}
|
||||
|
||||
addFileCard(file);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear file input, we handle on submit
|
||||
$fileInput.val('');
|
||||
};
|
||||
|
||||
// Browse link
|
||||
$browseLink.on('click', function(e) {
|
||||
e.preventDefault();
|
||||
$fileInput.trigger('click');
|
||||
});
|
||||
|
||||
// Drag and drop
|
||||
let dragCounter = 0;
|
||||
$uploadsDrop.on('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
}).on('dragenter', function(e) {
|
||||
e.preventDefault();
|
||||
dragCounter++;
|
||||
$uploadsDrop.addClass('highlight');
|
||||
}).on('dragleave', function(e) {
|
||||
e.preventDefault();
|
||||
dragCounter--;
|
||||
|
||||
if (dragCounter <= 0) {
|
||||
dragCounter = 0;
|
||||
$uploadsDrop.removeClass('highlight');
|
||||
}
|
||||
}).on('dragend', function() {
|
||||
dragCounter = 0;
|
||||
$uploadsDrop.removeClass('highlight');
|
||||
}).on('drop', function(e) {
|
||||
e.preventDefault();
|
||||
dragCounter = 0;
|
||||
$uploadsDrop.removeClass('highlight');
|
||||
handleFilesDrop(e.originalEvent.dataTransfer.files);
|
||||
});
|
||||
|
||||
// File input
|
||||
$fileInput.on('change', function(e) {
|
||||
handleFilesDrop(e.target.files);
|
||||
});
|
||||
|
||||
$uploadedFiles.on('click', '.remove-file-icon', function(ev) {
|
||||
const $file = $(ev.currentTarget).closest('.help-pane-upload-file');
|
||||
const filename = $file.find('.help-pane-upload-file-name').text();
|
||||
|
||||
// Remove from file store
|
||||
fileStore = fileStore.filter((f) => f.name !== filename);
|
||||
|
||||
// Remove from container
|
||||
$file.remove();
|
||||
|
||||
const messageLength =
|
||||
$uploadedFiles.find('.help-pane-upload-file').length;
|
||||
// Remove error message if num files isn't max
|
||||
if (messageLength < maxFiles) {
|
||||
removeFileErrorMessage();
|
||||
}
|
||||
|
||||
// Hide file container if it was the last removed message
|
||||
if (messageLength === 0) {
|
||||
$uploadedFiles.addClass('d-none');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Help main button
|
||||
$helpButton.on('click', () => {
|
||||
// If loader is active, skip
|
||||
if ($helpContainer.find('.help-pane-loader').length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($helpContainer.is(':visible')) {
|
||||
// Clear file store
|
||||
fileStore = [];
|
||||
|
||||
hideHelper();
|
||||
} else {
|
||||
$helpContainer.show();
|
||||
helperStep = 1;
|
||||
|
||||
// Render main panel
|
||||
renderPanelContent();
|
||||
|
||||
$('<div class="help-pane-overlay"></div>')
|
||||
.appendTo('body')
|
||||
.on('click', () => {
|
||||
if (helperStep === 1 || helperStep === 3) {
|
||||
hideHelper();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
23
ui/src/core/install.js
Normal file
23
ui/src/core/install.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Xibo - Digital Signage - http://www.xibo.org.uk
|
||||
* Copyright (C) 2009-2014 Daniel Garner
|
||||
*
|
||||
* This file is part of Xibo.
|
||||
*
|
||||
* Xibo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Xibo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
$(function() {
|
||||
|
||||
});
|
||||
3428
ui/src/core/xibo-calendar.js
Normal file
3428
ui/src/core/xibo-calendar.js
Normal file
File diff suppressed because it is too large
Load Diff
3134
ui/src/core/xibo-cms.js
Normal file
3134
ui/src/core/xibo-cms.js
Normal file
File diff suppressed because it is too large
Load Diff
1511
ui/src/core/xibo-datatables.js
Normal file
1511
ui/src/core/xibo-datatables.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user