jQuery(document).ready(function () {
// Check if debug feature is enabled
var debugEnabled = afm_object && afm_object.debug_enabled === '1';
// Global variables for error tracking
var hasErrors = false;
var currentEditor = null;
var currentErrors = [];
var lastButtonState = null;
var tooltipTimeout = null;
var lastTooltipLine = null;
var isSaveButtonClicked = false;
// CSS styles for error highlighting
var errorStyles = `
`;
jQuery('head').append(errorStyles);
if (1 == afm_object.hide_path) {
var custom_css = ``;
jQuery("head").append(custom_css);
}
var hide_preferences_css = ``;
jQuery('head').append(hide_preferences_css);
var fmakey = afm_object.nonce;
var fma_locale = afm_object.locale;
var fma_cm_theme = afm_object.cm_theme;
// PHP Debug Analysis function
function analyzePHPDebug(code, filename, callback) {
jQuery.ajax({
url: afm_object.ajaxurl,
type: 'POST',
data: {
action: 'fma_debug_php',
nonce: fmakey,
php_code: code,
filename: filename
},
success: function (response) {
if (callback && typeof callback === 'function') {
callback(response);
}
},
error: function () {
if (callback && typeof callback === 'function') {
callback({
valid: false,
debug_info: {},
message: 'Failed to analyze PHP code'
});
}
}
});
}
// Highlight error lines in CodeMirror
function highlightErrorLines(editor, errors) {
if (!editor || !errors || errors.length === 0) {
clearErrorHighlights(editor);
return;
}
currentErrors = errors;
for (var i = 0; i < editor.lineCount(); i++) {
editor.removeLineClass(i, 'background', 'fma-error-line');
editor.removeLineClass(i, 'text', 'fma-error-underline');
editor.setGutterMarker(i, 'fma-error-gutter', null);
}
errors.forEach(function (error) {
if (error.line && error.line > 0) {
var lineNumber = error.line - 1;
editor.addLineClass(lineNumber, 'background', 'fma-error-line');
editor.addLineClass(lineNumber, 'text', 'fma-error-underline');
var marker = document.createElement('div');
marker.className = 'fma-error-marker';
marker.innerHTML = '⚠️';
marker.title = error.message;
editor.setGutterMarker(lineNumber, 'fma-error-gutter', marker);
// Markers have titles which browser handles, but for the line text we use tooltips
}
});
hasErrors = true;
updateSaveCloseButton();
}
// Clear error highlights
function clearErrorHighlights(editor) {
if (!editor) return;
for (var i = 0; i < editor.lineCount(); i++) {
editor.removeLineClass(i, 'background', 'fma-error-line');
editor.removeLineClass(i, 'text', 'fma-error-underline');
editor.setGutterMarker(i, 'fma-error-gutter', null);
}
hideErrorTooltip();
currentErrors = [];
hasErrors = false;
updateSaveCloseButton();
}
// Show error tooltip on hover
function showErrorTooltip(event, error) {
if (tooltipTimeout) clearTimeout(tooltipTimeout);
if (lastTooltipLine === error.line) {
// Just update position if moving within same line
var tooltip = jQuery('.fma-error-tooltip');
if (tooltip.length) {
var x = event.pageX + 10;
var y = event.pageY - 15;
tooltip.css({ left: x + 'px', top: (y - tooltip.outerHeight()) + 'px' });
}
return;
}
tooltipTimeout = setTimeout(function () {
jQuery('.fma-error-tooltip').remove();
var tooltipHtml = `
⚠️ PHP Error
${error.message}
Line ${error.line}
`;
var tooltip = jQuery(tooltipHtml).appendTo('body');
var x = event.pageX + 10;
var y = event.pageY - 15;
tooltip.css({ left: x + 'px', top: (y - tooltip.outerHeight()) + 'px' });
lastTooltipLine = error.line;
}, 50); // Faster response
}
// Hide error tooltip
function hideErrorTooltip() {
if (tooltipTimeout) clearTimeout(tooltipTimeout);
jQuery('.fma-error-tooltip').remove();
lastTooltipLine = null;
}
// Get error for specific line number
function getErrorForLine(lineNumber) {
for (var i = 0; i < currentErrors.length; i++) {
if (currentErrors[i].line === lineNumber + 1) return currentErrors[i];
}
return null;
}
// Update Save & Close button state
function updateSaveCloseButton() {
var currentState = hasErrors ? 'disabled' : 'enabled';
if (lastButtonState === currentState) return;
lastButtonState = currentState;
var selectors = [
'.elfinder-button-save-close', '.elfinder-button-save',
'[title*="Save"]', '[title*="save"]',
'button[title*="Save"]', 'button[title*="save"]',
'button:contains("Save")', 'button:contains("save")'
];
var saveCloseBtn = jQuery(selectors.join(', ')).filter(':visible');
if (saveCloseBtn.length > 0) {
if (hasErrors) {
saveCloseBtn.addClass('fma-save-close-disabled').attr('disabled', 'disabled');
saveCloseBtn.css({ opacity: '0.5', cursor: 'not-allowed' });
saveCloseBtn.off('click.fma-disable').on('click.fma-disable', function (e) {
e.preventDefault(); return false;
});
} else {
saveCloseBtn.removeClass('fma-save-close-disabled').removeAttr('disabled');
saveCloseBtn.css({ opacity: '1', cursor: 'pointer' });
saveCloseBtn.off('click.fma-disable');
}
}
}
// Periodic check for Save button
setInterval(function () {
if (currentEditor && hasErrors) updateSaveCloseButton();
}, 5000);
// Show error popup on save
function showErrorSavePopup(errors, callback) {
var errorList = errors.map(function (error) {
return `Line ${error.line}: ${error.message}`;
}).join(' ');
var popupHtml = `
PHP Syntax Errors Found
${errorList}
`;
jQuery('.fma-modal-overlay').remove();
var popup = jQuery(popupHtml).appendTo('body');
popup.find('.fma-error-okay').on('click', function () {
popup.remove();
if (callback) callback(false);
});
popup.find('.fma-error-save-anyway').on('click', function () {
popup.remove();
if (callback) callback(true);
});
}
// Success Modal
function showSuccessModal(message) {
if (jQuery('.fma-modal-overlay').length > 0) return;
var modalHtml = `
✓
${message}
`;
var modal = jQuery(modalHtml).appendTo('body');
modal.find('.fma-modal-close').on('click', function () { modal.remove(); });
setTimeout(function () { modal.remove(); }, 3000);
}
// Initialize elFinder
var elfinder_object = jQuery('#file_manager_advanced').elfinder({
cssAutoLoad: false,
url: afm_object.ajaxurl,
customData: {
action: 'fma_load_fma_ui',
_fmakey: fmakey,
},
defaultView: 'list',
height: 500,
lang: fma_locale,
ui: afm_object.ui,
commandsOptions: {
edit: {
mimes: [],
editors: [{
mimes: ['text/plain', 'text/html', 'text/javascript', 'text/css', 'text/x-php', 'application/x-php'],
info: { name: 'Code Editor' },
load: function (textarea) {
var mimeType = this.file.mime;
var filename = this.file.name;
var self = this;
var editor = CodeMirror.fromTextArea(textarea, {
mode: mimeType,
indentUnit: 4,
lineNumbers: true,
theme: fma_cm_theme,
gutters: ["CodeMirror-lint-markers", "CodeMirror-linenumbers"]
});
editor.fma_file_info = { filename: filename, mime: mimeType, hash: self.file.hash };
if (debugEnabled && filename.toLowerCase().endsWith('.php')) {
editor.on('change', function () {
var code = editor.getValue();
analyzePHPDebug(code, filename, function (result) {
if (result.valid) {
clearErrorHighlights(editor);
} else {
highlightErrorLines(editor, result.errors);
}
});
});
}
// Hover tooltip: CodeMirror coordsChar() "page" mode needs document coords (pageX/pageY)
var wrapper = editor.getWrapperElement();
jQuery(wrapper).on('mousemove.fma-debug', function (e) {
if (!hasErrors || currentErrors.length === 0) return;
var docLeft = e.pageX != null ? e.pageX : (e.clientX + (window.scrollX || document.documentElement.scrollLeft || 0));
var docTop = e.pageY != null ? e.pageY : (e.clientY + (window.scrollY || document.documentElement.scrollTop || 0));
var coords = editor.coordsChar({ left: docLeft, top: docTop }, 'page');
if (coords.outside) return;
var error = getErrorForLine(coords.line);
if (error) {
showErrorTooltip(e, error);
} else {
hideErrorTooltip();
}
});
jQuery(wrapper).on('mouseleave.fma-debug', function () {
hideErrorTooltip();
});
currentEditor = editor;
return editor;
},
close: function (textarea, instance) {
if (instance) clearErrorHighlights(instance);
currentEditor = null;
},
save: function (textarea, editor) {
var code = editor.getValue();
var filename = editor.fma_file_info ? editor.fma_file_info.filename : 'unknown.php';
if (filename.toLowerCase().endsWith('.php') && hasErrors) {
analyzePHPDebug(code, filename, function (result) {
if (!result.valid && result.errors) {
showErrorSavePopup(result.errors, function (saveAnyway) {
if (saveAnyway) {
jQuery(textarea).val(code);
if (typeof editor.save === 'function') editor.save();
}
});
}
});
return false;
}
jQuery(textarea).val(code);
return true;
}
}]
}
},
workerBaseUrl: afm_object.plugin_url + 'application/library/js/worker/',
});
// Override search command to add contentTag parameter for content search (after init)
function applyContentSearchOverride(fm) {
if (!fm || !fm._commands || !fm._commands.search) return;
if (fm._commands.search._fmaContentSearchPatched) return;
fm._commands.search._fmaContentSearchPatched = true;
var originalSearchExec = fm._commands.search.exec;
// Override exec method to add contentTag for SearchTag type
fm._commands.search.exec = function(q, target, mime, type) {
var self = this;
var sType = type || '';
// If SearchTag type, we need to send contentTag parameter
if (sType === 'SearchTag' && q) {
var contentTag = q; // Store the tag
var fm = this.fm;
var reqDef = [];
var onlyMimes = fm.options.onlyMimes;
var phash, targetVolids = [];
// Custom setType function that encodes tag in q parameter for SearchTag
var setType = function(data) {
if (sType && sType !== 'SearchName' && sType !== 'SearchMime') {
data.type = sType;
}
if (sType === 'SearchTag') {
data.q = '__CONTENT_SEARCH__:' + contentTag;
}
return data;
};
var rootCnt;
// Process target
if (typeof target == 'object') {
mime = target.mime || '';
target = target.target || '';
}
target = target ? target : '';
// Process mimes
if (mime) {
mime = jQuery.trim(mime).replace(',', ' ').split(' ');
if (onlyMimes.length) {
mime = jQuery.map(mime, function(m) {
m = jQuery.trim(m);
return m && (jQuery.inArray(m, onlyMimes) !== -1
|| jQuery.grep(onlyMimes, function(om) { return m.indexOf(om) === 0 ? true : false; }).length
) ? m : null;
});
}
} else {
mime = [].concat(onlyMimes);
}
fm.trigger('searchstart', setType({query: '', target: target, mimes: mime}));
if (!onlyMimes.length || mime.length) {
if (target === '' && fm.api >= 2.1) {
rootCnt = Object.keys(fm.roots).length;
jQuery.each(fm.roots, function(id, hash) {
reqDef.push(fm.request({
data: setType({cmd: 'search', target: hash, mimes: mime}),
notify: {type: 'search', cnt: 1, hideCnt: (rootCnt > 1 ? false : true)},
cancel: true,
preventDone: true
}));
});
} else {
reqDef.push(fm.request({
data: setType({cmd: 'search', target: target, mimes: mime}),
notify: {type: 'search', cnt: 1, hideCnt: true},
cancel: true,
preventDone: true
}));
if (target !== '' && fm.api >= 2.1 && Object.keys(fm.leafRoots).length) {
jQuery.each(fm.leafRoots, function(hash, roots) {
phash = hash;
while (phash) {
if (target === phash) {
jQuery.each(roots, function() {
var f = fm.file(this);
f && f.volumeid && targetVolids.push(f.volumeid);
reqDef.push(fm.request({
data: setType({cmd: 'search', target: this, mimes: mime}),
notify: {type: 'search', cnt: 1, hideCnt: false},
cancel: true,
preventDone: true
}));
});
}
phash = (fm.file(phash) || {}).phash;
}
});
}
}
} else {
reqDef = [jQuery.Deferred().resolve({files: []})];
}
fm.searchStatus.mixed = (reqDef.length > 1) ? targetVolids : false;
return jQuery.when.apply(jQuery, reqDef).done(function(data) {
var argLen = arguments.length, i;
data.warning && fm.error(data.warning);
if (argLen > 1) {
data.files = (data.files || []);
for (i = 1; i < argLen; i++) {
arguments[i].warning && fm.error(arguments[i].warning);
if (arguments[i].files) {
data.files.push.apply(data.files, arguments[i].files);
}
}
}
data.files && data.files.length && fm.cache(data.files);
fm.lazy(function() {
fm.trigger('search', data);
}).then(function() {
return fm.lazy(function() {
fm.trigger('searchdone');
});
}).then(function() {
data.sync && fm.sync();
});
});
} else {
return originalSearchExec.call(self, q, target, mime, type);
}
};
}
if (elfinder_object && elfinder_object.length) {
var fm = elfinder_object.elfinder('instance');
if (fm) {
fm.one('init', function() { applyContentSearchOverride(fm); });
applyContentSearchOverride(fm);
}
}
// When user clears search and presses Enter (or closes search), go back to folder they were in before search
if (elfinder_object && elfinder_object.length) {
var fm = elfinder_object.elfinder('instance');
if (fm) {
var cwdBeforeSearch = null;
fm.bind('searchstart', function() {
var cwd = fm.cwd();
if (cwd && cwd.hash) {
cwdBeforeSearch = cwd.hash;
}
});
fm.bind('searchend', function() {
if (cwdBeforeSearch && fm.file(cwdBeforeSearch)) {
fm.exec('open', cwdBeforeSearch);
}
cwdBeforeSearch = null;
});
}
}
// Add Content search radio button after elFinder is initialized
if (elfinder_object && elfinder_object.length) {
var fm = elfinder_object.elfinder('instance');
if (fm) {
var addContentRadioButton = function() {
var searchMenu = jQuery('.elfinder-button-search-menu');
if (searchMenu.length) {
var namespace = fm.namespace || 'elfinder-';
var id = function(name) {
return namespace + (fm.escape ? fm.escape(name) : name);
};
var searchTypeSet = searchMenu.find('.elfinder-search-type');
if (searchTypeSet.length && !searchTypeSet.find('#' + id('SearchTag')).length) {
var tagRadio = jQuery('');
var tagLabel = jQuery('');
searchTypeSet.append(tagRadio).append(tagLabel);
searchTypeSet.buttonset('refresh');
// Remember search type when target is "Here", so we can restore only when user had chosen Content then switched to "All"
var lastSearchTypeWhenHere = null;
var updateContentState = function() {
var isHere = jQuery('#' + id('SearchFromCwd')).prop('checked');
var tagRadio = jQuery('#' + id('SearchTag'));
var tagLabel = tagRadio.next('label');
if (tagRadio.length) {
if (isHere) {
tagRadio.prop('disabled', false);
tagLabel.css({'opacity': '1', 'cursor': 'pointer'});
// Restore Content when switching back to "Here" if it was selected before
if (lastSearchTypeWhenHere === 'SearchTag') {
tagRadio.prop('checked', true);
searchTypeSet.buttonset('refresh');
}
} else {
tagRadio.prop('disabled', true);
tagLabel.css({'opacity': '0.5', 'cursor': 'not-allowed'});
if (tagRadio.prop('checked')) {
lastSearchTypeWhenHere = 'SearchTag'; // was Content before switching to All
jQuery('#' + id('SearchName')).prop('checked', true).trigger('change');
searchTypeSet.buttonset('refresh');
}
}
}
};
// When user changes search type while target is "Here", remember it
searchMenu.off('change.contentsearch', 'input[name="serchcol"]').on('change.contentsearch', 'input[name="serchcol"]', function() {
if (jQuery('#' + id('SearchFromCwd')).prop('checked')) {
var val = jQuery(this).val();
if (val) lastSearchTypeWhenHere = val;
}
});
searchMenu.off('change.contentsearch', 'input[name="serchfrom"]').on('change.contentsearch', 'input[name="serchfrom"]', updateContentState);
updateContentState();
}
}
};
jQuery(document).on('click focus', '.elfinder-button-search input[type="text"]', function() {
setTimeout(addContentRadioButton, 200);
});
fm.bind('open', function() {
setTimeout(addContentRadioButton, 500);
});
setTimeout(addContentRadioButton, 1000);
}
}
});