dolibarr.filearchive/js/docbrowser.js.php
data cbf04e6cc9 FileArchiv: Tile/Gallery view with toggle buttons
- Added formObjectOptions hook for document pages
- CSS/JS injected via DOMContentLoaded for proper timing
- Toggle between list and tile view (localStorage persisted)
- Lightbox for image gallery with keyboard navigation
- Fixed hook contexts (productdocuments, invoicedocuments, etc.)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-19 16:59:19 +01:00

350 lines
12 KiB
PHP

<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
* JavaScript for FileArchiv Document Browser
*/
if (!defined('NOTOKENRENEWAL')) {
define('NOTOKENRENEWAL', '1');
}
if (!defined('NOLOGIN')) {
define('NOLOGIN', '1');
}
if (!defined('NOCSRFCHECK')) {
define('NOCSRFCHECK', '1');
}
// Load Dolibarr environment for translations
$res = 0;
if (!$res && file_exists("../../main.inc.php")) {
$res = @include "../../main.inc.php";
}
if (!$res && file_exists("../../../main.inc.php")) {
$res = @include "../../../main.inc.php";
}
if (!$res && file_exists("../../../../main.inc.php")) {
$res = @include "../../../../main.inc.php";
}
header('Content-Type: application/javascript; charset=UTF-8');
// Load translations
$langs->load('filearchiv@filearchiv');
$ajaxUrl = dol_buildpath('/filearchiv/ajax/getdocuments.php', 1);
$addFileUrl = dol_buildpath('/filearchiv/ajax/addfile.php', 1);
?>
/**
* FileArchiv Document Browser
* Injects document browser into email presend forms
*/
(function() {
'use strict';
// Only run on presend pages
if (window.location.href.indexOf('action=presend') === -1) {
return;
}
// Configuration
var config = {
ajaxUrl: '<?php echo dol_escape_js($ajaxUrl); ?>',
addFileUrl: '<?php echo dol_escape_js($addFileUrl); ?>',
token: '',
element: '',
id: 0,
trackid: '',
translations: {
browseDocuments: '<?php echo dol_escape_js($langs->trans("BrowseRelatedDocuments")); ?>',
selectDocuments: '<?php echo dol_escape_js($langs->trans("SelectDocumentsToAttach")); ?>',
loading: '<?php echo dol_escape_js($langs->trans("Loading")); ?>',
noDocuments: '<?php echo dol_escape_js($langs->trans("NoRelatedDocumentsFound")); ?>',
errorLoading: '<?php echo dol_escape_js($langs->trans("ErrorLoadingDocuments")); ?>',
filesSelected: '<?php echo dol_escape_js($langs->trans("FilesSelected")); ?>',
cancel: '<?php echo dol_escape_js($langs->trans("Cancel")); ?>',
addSelected: '<?php echo dol_escape_js($langs->trans("AddSelectedFiles")); ?>',
adding: '<?php echo dol_escape_js($langs->trans("Adding")); ?>',
errorAdding: '<?php echo dol_escape_js($langs->trans("ErrorAddingFiles")); ?>'
}
};
// Detect element type and ID from URL
function detectContext() {
var url = window.location.href;
var params = new URLSearchParams(window.location.search);
config.id = parseInt(params.get('id')) || 0;
config.token = typeof token !== 'undefined' ? token : '';
// Detect element type from URL path
if (url.indexOf('/comm/propal/') !== -1) {
config.element = 'propal';
} else if (url.indexOf('/commande/') !== -1) {
config.element = 'commande';
} else if (url.indexOf('/compta/facture/') !== -1) {
config.element = 'facture';
} else if (url.indexOf('/expedition/') !== -1) {
config.element = 'expedition';
} else if (url.indexOf('/fourn/commande/') !== -1) {
config.element = 'order_supplier';
} else if (url.indexOf('/fourn/facture/') !== -1) {
config.element = 'invoice_supplier';
}
// Get trackid from form
var trackidInput = document.querySelector('input[name="trackid"]');
if (trackidInput) {
config.trackid = trackidInput.value;
}
// Get token from form
var tokenInput = document.querySelector('input[name="token"]');
if (tokenInput) {
config.token = tokenInput.value;
}
}
// Create and inject the document browser UI
function injectDocumentBrowser() {
// Find the mail form
var mailForm = document.getElementById('mailform');
if (!mailForm) {
mailForm = document.querySelector('form[name="mailform"]');
}
if (!mailForm) {
return;
}
// Find attachment section (look for file input or attachment table)
var attachSection = mailForm.querySelector('input[name="addedfile"]');
if (!attachSection) {
attachSection = mailForm.querySelector('.linked-medias');
}
if (!attachSection) {
// Try to find any file-related section
attachSection = mailForm.querySelector('table.liste');
}
// Create button container
var buttonHtml = '<div class="filearchiv-docbrowser-section">' +
'<div class="filearchiv-docbrowser-title">' +
'<i class="fas fa-folder-open"></i> ' + config.translations.browseDocuments +
'</div>' +
'<button type="button" class="filearchiv-btn filearchiv-btn-secondary" onclick="FileArchivDocBrowser.open()">' +
'<i class="fas fa-search"></i> ' + config.translations.selectDocuments +
'</button>' +
'</div>';
// Create modal
var modalHtml = '<div class="filearchiv-modal-overlay" id="filearchiv-docbrowser-modal">' +
'<div class="filearchiv-modal">' +
'<div class="filearchiv-modal-header">' +
'<h3><i class="fas fa-folder-open"></i> ' + config.translations.selectDocuments + '</h3>' +
'<span class="filearchiv-modal-close" onclick="FileArchivDocBrowser.close()">&times;</span>' +
'</div>' +
'<div class="filearchiv-modal-body" id="filearchiv-docbrowser-content">' +
'<div class="filearchiv-loading">' +
'<i class="fas fa-spinner fa-spin"></i>' +
'<div>' + config.translations.loading + '...</div>' +
'</div>' +
'</div>' +
'<div class="filearchiv-modal-footer">' +
'<span class="filearchiv-selected-count">' +
'<span id="filearchiv-selected-num">0</span> ' + config.translations.filesSelected +
'</span>' +
'<div>' +
'<button type="button" class="filearchiv-btn filearchiv-btn-secondary" onclick="FileArchivDocBrowser.close()">' +
config.translations.cancel +
'</button> ' +
'<button type="button" class="filearchiv-btn filearchiv-btn-primary" id="filearchiv-add-btn" onclick="FileArchivDocBrowser.addSelected()" disabled>' +
'<i class="fas fa-plus"></i> ' + config.translations.addSelected +
'</button>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
// Insert button before attachment section or at end of form
var container = document.createElement('div');
container.innerHTML = buttonHtml;
if (attachSection && attachSection.parentNode) {
attachSection.parentNode.insertBefore(container.firstChild, attachSection);
} else {
// Insert before submit button
var submitBtn = mailForm.querySelector('input[type="submit"], button[type="submit"]');
if (submitBtn && submitBtn.parentNode) {
submitBtn.parentNode.insertBefore(container.firstChild, submitBtn);
} else {
mailForm.appendChild(container.firstChild);
}
}
// Add modal to body
document.body.insertAdjacentHTML('beforeend', modalHtml);
}
// Document Browser Controller
window.FileArchivDocBrowser = {
loaded: false,
selectedFiles: [],
open: function() {
document.getElementById('filearchiv-docbrowser-modal').classList.add('active');
document.body.style.overflow = 'hidden';
if (!this.loaded) {
this.loadDocuments();
}
},
close: function() {
document.getElementById('filearchiv-docbrowser-modal').classList.remove('active');
document.body.style.overflow = '';
},
loadDocuments: function() {
var self = this;
var url = config.ajaxUrl + '?element=' + config.element + '&id=' + config.id + '&token=' + config.token;
fetch(url)
.then(function(response) { return response.json(); })
.then(function(data) {
self.loaded = true;
self.renderDocuments(data);
})
.catch(function(error) {
console.error('Error loading documents:', error);
document.getElementById('filearchiv-docbrowser-content').innerHTML =
'<div class="filearchiv-empty"><i class="fas fa-exclamation-triangle"></i> ' + config.translations.errorLoading + '</div>';
});
},
renderDocuments: function(data) {
var self = this;
var container = document.getElementById('filearchiv-docbrowser-content');
if (!data.success || !data.categories || data.categories.length === 0) {
container.innerHTML = '<div class="filearchiv-empty"><i class="fas fa-folder-open"></i><br>' + config.translations.noDocuments + '</div>';
return;
}
var html = '';
data.categories.forEach(function(category, catIndex) {
html += '<div class="filearchiv-category' + (catIndex === 0 ? ' open' : '') + '" id="cat-' + catIndex + '">';
html += '<div class="filearchiv-category-header" onclick="FileArchivDocBrowser.toggleCategory(' + catIndex + ')">';
html += '<i class="fas ' + category.icon + '"></i>';
html += '<span>' + self.escapeHtml(category.title) + '</span>';
html += '<span class="count">' + category.files.length + '</span>';
html += '<i class="fas fa-chevron-down toggle-icon"></i>';
html += '</div>';
html += '<div class="filearchiv-category-body">';
category.files.forEach(function(file, fileIndex) {
var fileId = 'file-' + catIndex + '-' + fileIndex;
html += '<div class="filearchiv-file-item">';
html += '<input type="checkbox" id="' + fileId + '" onchange="FileArchivDocBrowser.toggleFile(this, \'' + self.escapeHtml(file.fullpath) + '\', \'' + self.escapeHtml(file.name) + '\')">';
html += '<i class="fas ' + file.icon + ' file-icon"></i>';
html += '<div class="file-info">';
html += '<div class="file-name">' + self.escapeHtml(file.name) + '</div>';
html += '<div class="file-meta">' + file.size_formatted + ' - ' + file.date + '</div>';
if (file.product_ref) {
html += '<span class="file-source">' + self.escapeHtml(file.product_ref) + ': ' + self.escapeHtml(file.product_label) + '</span>';
} else if (file.object_ref) {
html += '<span class="file-source">' + self.escapeHtml(file.object_ref) + '</span>';
}
html += '</div>';
html += '</div>';
});
html += '</div></div>';
});
container.innerHTML = html;
},
escapeHtml: function(text) {
if (!text) return '';
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
},
toggleCategory: function(index) {
var cat = document.getElementById('cat-' + index);
cat.classList.toggle('open');
},
toggleFile: function(checkbox, filepath, filename) {
if (checkbox.checked) {
this.selectedFiles.push({ path: filepath, name: filename });
} else {
this.selectedFiles = this.selectedFiles.filter(function(f) {
return f.path !== filepath;
});
}
this.updateSelectedCount();
},
updateSelectedCount: function() {
document.getElementById('filearchiv-selected-num').textContent = this.selectedFiles.length;
document.getElementById('filearchiv-add-btn').disabled = this.selectedFiles.length === 0;
},
addSelected: function() {
if (this.selectedFiles.length === 0) return;
var self = this;
var addBtn = document.getElementById('filearchiv-add-btn');
addBtn.disabled = true;
addBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + config.translations.adding + '...';
var promises = this.selectedFiles.map(function(file) {
return fetch(config.addFileUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'filepath=' + encodeURIComponent(file.path) +
'&filename=' + encodeURIComponent(file.name) +
'&trackid=' + encodeURIComponent(config.trackid) +
'&token=' + encodeURIComponent(config.token)
});
});
Promise.all(promises)
.then(function() {
window.location.reload();
})
.catch(function(error) {
console.error('Error adding files:', error);
addBtn.disabled = false;
addBtn.innerHTML = '<i class="fas fa-plus"></i> ' + config.translations.addSelected;
alert(config.translations.errorAdding);
});
}
};
// Initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
detectContext();
if (config.element && config.id) {
injectDocumentBrowser();
}
});
} else {
detectContext();
if (config.element && config.id) {
injectDocumentBrowser();
}
}
// Close modal on Escape
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
var modal = document.getElementById('filearchiv-docbrowser-modal');
if (modal && modal.classList.contains('active')) {
FileArchivDocBrowser.close();
}
}
});
})();