Files
Bookshelf/umbra-bookshelf/library/assets/js/admin.js
bdm dec732931c Consolidate three plugins into Umbra Bookshelf
Merge BD Bookshelf (0.1.0), BD BookShelfHelper (2.0.0) and BD SimpleEmail
(2.0.0) into a single plugin released as Umbra Bookshelf 1.0.0, with the
BD Bookshelf theme left as a separate, unrenamed artifact.

Modules
  includes/books/    catalogue, ISBN/ASIN, series, retail, ratings, shortcodes,
                     SEO, GDPR, scrapers, admin, CSV/post import
  includes/library/  book post type + series taxonomy, pen names, data entry,
                     shortlinks, reviews, migrate
  includes/email/    subscribers, newsletters, composition, sending

Renamed: plugin headers, text domains (all three -> umbra-bookshelf), classes
(BD_*/BDM_* -> Umbra_*), constants (-> UMBRA_BOOKSHELF_*), asset handles, JS
globals and admin slugs.

Deliberately not renamed -- persisted contract with live installs and with
published post content: table names, option keys, post meta keys, transients,
nonce actions, cron hooks, AJAX actions, shortcodes ([bd-book], [bd-books],
[bdm_signup]), post type `book`, taxonomy `series`, cookies and CSS classes.
BD_BookShelfHelper survives only as a class_alias for the unmodified theme.

Collisions resolved
  * class-umbra-theme-compatibility.php declared BD_BookShelfHelper, the same
    name as the library module's helper. Latent while the plugins were
    separate (only fired when both were active), but an unconditional fatal
    once merged. Renamed to Umbra_Theme_Compatibility with a class_alias.
  * All three modules called add_menu_page. One top-level menu is now
    registered by the bootstrap at admin_menu priority 9.
  * BD Bookshelf and BookShelfHelper both claimed bd-bookshelf-settings, leaving
    one Settings screen unreachable -> umbra-bookshelf-settings and
    umbra-bookshelf-library-settings.
  * Catalogue and library both enqueued bd-bookshelf-admin; the second enqueue
    was silently discarded -> unique handles per module.
  * Umbra_Admin and Umbra_Migration both registered bd_bookshelf_import_csv, so
    one handler silently replaced the other. The import page now lives only in
    Umbra_Admin (the variant wired to the admin JS); Umbra_Migration keeps only
    the preview/run endpoints it uniquely provides.
  * Modules were constructed from an init callback, so their own init
    registrations raced the hook being dispatched. Now constructed on
    plugins_loaded behind a re-entrancy guard.

Also
  * Missing assets created: assets/images/default-cover.png (referenced but
    never committed, so every cover-less book 404'd) and email/assets/js/
    admin.js (enqueued on every email admin page but never committed).
  * Library module's strpos($hook, 'bd-bookshelf') asset check matched every
    catalogue and email screen once slugs shared the umbra-bookshelf- prefix;
    now matched against its own slug list.
  * Consolidated uninstall deduplicates the double drop of bd_books/bd_series.
  * Specs aligned to the new name; pre-merge specs retained under
    spec/archive/ with provenance notes.
  * Tests added: tests/verify_structure.py (static) and tests/smoke_runtime.php
    (WordPress-stub runtime boot, front-end and admin contexts).

Deferred and recorded in docs/KNOWN-ISSUES.md: the catalogue and library
modules still create bd_books and bd_series with two incompatible schemas
(pre-existing, now unconditional), and Goodreads/Amazon scraping is still
implemented twice on shared cron hooks.
2026-10-18 19:55:36 -07:00

410 lines
14 KiB
JavaScript

// Copyright 2026 Umbra Tools (umbratools.com)
// SPDX-License-Identifier: Apache-2.0
/**
* BD BookShelfHelper Admin JavaScript
* Enhanced Data Entry Interface
*/
(function($) {
'use strict';
const BDDataEntry = {
currentLanguage: 'en',
autoSaveInterval: null,
isDirty: false,
saveInProgress: false,
init: function() {
this.initLanguageTabs();
this.initSectionCollapse();
this.initFieldValidation();
this.initAutoSave();
this.initPreviewButton();
this.initDirtyTracking();
this.initSaveButton();
this.initFieldBlurSave();
},
/**
* Initialize language tabs
*/
initLanguageTabs: function() {
const tabs = $('.bd-lang-tab');
const contents = $('.bd-language-content');
tabs.on('click', function() {
const lang = $(this).data('lang');
tabs.removeClass('active');
$(this).addClass('active');
contents.removeClass('active');
$('#bd-lang-' + lang).addClass('active');
BDDataEntry.currentLanguage = lang;
});
},
/**
* Initialize section collapse/expand
*/
initSectionCollapse: function() {
$('.bd-section-header').on('click', function() {
const header = $(this);
const content = header.next('.bd-section-content');
header.toggleClass('collapsed');
content.toggleClass('collapsed');
});
},
/**
* Initialize field validation
*/
initFieldValidation: function() {
// ISBN-13 validation
$('input[name="isbn"]').on('blur', function() {
const value = $(this).val().replace(/[-\s]/g, '');
if (value && !/^\d{13}$/.test(value)) {
BDDataEntry.setFieldInvalid($(this), 'ISBN-13 must be exactly 13 digits');
} else if (value) {
BDDataEntry.setFieldValid($(this));
}
});
// ASIN validation
$('input[name="asin"]').on('blur', function() {
const value = $(this).val().replace(/[-\s]/g, '');
if (value && !/^[A-Z0-9]{10}$/i.test(value)) {
BDDataEntry.setFieldInvalid($(this), 'ASIN must be exactly 10 alphanumeric characters');
} else if (value) {
BDDataEntry.setFieldValid($(this));
}
});
// URL validation
$('input[type="url"]').on('blur', function() {
const value = $(this).val();
if (value && !BDDataEntry.isValidUrl(value)) {
BDDataEntry.setFieldInvalid($(this), 'Please enter a valid URL');
} else if (value) {
BDDataEntry.setFieldValid($(this));
}
});
// Date validation
$('input[type="date"]').on('blur', function() {
const value = $(this).val();
if (value && !BDDataEntry.isValidDate(value)) {
BDDataEntry.setFieldInvalid($(this), 'Please enter a valid date (YYYY-MM-DD)');
} else if (value) {
BDDataEntry.setFieldValid($(this));
}
});
// Required field validation on submit
$('#bd-data-entry-form').on('submit', function(e) {
let isValid = true;
$('.bd-field.required input, .bd-field.required select, .bd-field.required textarea').each(function() {
if (!$(this).val() || !$(this).val().trim()) {
BDDataEntry.setFieldInvalid($(this), 'This field is required');
isValid = false;
}
});
// Validate ISBN
const isbn = $('input[name="isbn"]').val().replace(/[-\s]/g, '');
if (isbn && !/^\d{13}$/.test(isbn)) {
BDDataEntry.setFieldInvalid($('input[name="isbn"]'), 'ISBN-13 must be exactly 13 digits');
isValid = false;
}
// Validate ASIN
const asin = $('input[name="asin"]').val().replace(/[-\s]/g, '');
if (asin && !/^[A-Z0-9]{10}$/i.test(asin)) {
BDDataEntry.setFieldInvalid($('input[name="asin"]'), 'ASIN must be exactly 10 alphanumeric characters');
isValid = false;
}
if (!isValid) {
e.preventDefault();
BDDataEntry.showStatus('Please fix the validation errors before saving.', 'error');
$('html, body').animate({
scrollTop: $('.bd-data-entry input.invalid').first().offset().top - 100
}, 300);
}
});
},
/**
* Set field as valid
*/
setFieldValid: function($field) {
$field.removeClass('invalid').addClass('valid');
$field.next('.bd-validation-message').remove();
},
/**
* Set field as invalid
*/
setFieldInvalid: function($field, message) {
$field.removeClass('valid').addClass('invalid');
let $msg = $field.next('.bd-validation-message');
if (!$msg.length) {
$msg = $('<span class="bd-validation-message"></span>');
$field.after($msg);
}
$msg.text(message);
},
/**
* Check if URL is valid
*/
isValidUrl: function(string) {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
},
/**
* Check if date is valid
*/
isValidDate: function(string) {
return /^\d{4}-\d{2}-\d{2}$/.test(string) && !isNaN(Date.parse(string));
},
/**
* Initialize auto-save on interval
*/
initAutoSave: function() {
// Auto-save every 30 seconds
this.autoSaveInterval = setInterval(function() {
if (BDDataEntry.isDirty && !BDDataEntry.saveInProgress) {
BDDataEntry.autoSave();
}
}, 30000);
// Save on page unload (beforeunload)
$(window).on('beforeunload', function() {
if (BDDataEntry.isDirty && !BDDataEntry.saveInProgress) {
BDDataEntry.autoSave(true);
}
});
},
/**
* Initialize save on field blur
*/
initFieldBlurSave: function() {
// Save individual field on blur
$('#bd-data-entry-form').on('blur', 'input, select, textarea', function() {
const $field = $(this);
const fieldName = $field.attr('name');
// Skip certain fields
if (!fieldName || fieldName === 'post_id' || fieldName === '_wpnonce' || fieldName === '_wp_http_referer') {
return;
}
if (BDDataEntry.isDirty && !BDDataEntry.saveInProgress) {
// Small delay to allow multiple field changes
clearTimeout(BDDataEntry.blurSaveTimeout);
BDDataEntry.blurSaveTimeout = setTimeout(function() {
BDDataEntry.autoSave();
}, 500);
}
});
},
/**
* Initialize save button
*/
initSaveButton: function() {
$('#bd-data-entry-form').on('submit', function(e) {
// Don't prevent default - let the form submit normally for full save
// But do show loading state
BDDataEntry.showSaveIndicator('saving');
});
// Listen for the submit button click
$('#bd_save_data_entry').on('click', function() {
BDDataEntry.saveInProgress = true;
BDDataEntry.showSaveIndicator('saving');
});
},
/**
* Show save indicator
*/
showSaveIndicator: function(status) {
const $indicator = $('.bd-autosave-indicator');
if (!$indicator.length) {
// Create indicator if it doesn't exist
$('#bd-data-entry-form').after('<div class="bd-autosave-indicator"></div>');
}
const $ind = $('.bd-autosave-indicator');
if (status === 'saving') {
$ind.removeClass('saved error').addClass('saving visible').text('Saving...');
} else if (status === 'saved') {
$ind.removeClass('saving error').addClass('saved visible').text('Saved successfully!');
BDDataEntry.isDirty = false;
setTimeout(function() {
$ind.removeClass('visible saved');
}, 3000);
} else if (status === 'error') {
$ind.removeClass('saving saved').addClass('error visible').text('Save failed. Please try again.');
setTimeout(function() {
$ind.removeClass('visible error');
}, 5000);
}
},
/**
* Perform auto-save
*/
autoSave: function(sync) {
if (this.saveInProgress || !this.isDirty) {
return;
}
this.saveInProgress = true;
this.showSaveIndicator('saving');
const formData = $('#bd-data-entry-form').serialize();
const postId = this.getPostId();
if (!postId) {
this.showSaveIndicator('error');
this.saveInProgress = false;
return;
}
formData += '&action=bdm_autosave&post_id=' + postId;
$.ajax({
url: ajaxurl,
type: 'POST',
data: formData,
success: function(response) {
if (response.success) {
BDDataEntry.isDirty = false;
BDDataEntry.showSaveIndicator('saved');
// Mark the form as saved (removes dirty state)
$('#bd-data-entry-form').data('saved', true);
} else {
BDDataEntry.showSaveIndicator('error');
}
},
error: function() {
BDDataEntry.showSaveIndicator('error');
},
complete: function() {
BDDataEntry.saveInProgress = false;
},
async: sync !== true
});
},
/**
* Get current post ID
*/
getPostId: function() {
// Try to get from hidden post_id input (data entry form)
const postIdFromForm = $('input[name="post_id"]').val();
if (postIdFromForm) {
return postIdFromForm;
}
// Fallback to post_ID (standard WordPress)
return $('#post_ID').val() || 0;
},
/**
* Initialize preview button
*/
initPreviewButton: function() {
$('#bd-preview-btn').on('click', function(e) {
e.preventDefault();
// Save first
$('#bd-data-entry-form').trigger('submit');
const postId = BDDataEntry.getPostId();
if (postId) {
window.open(
post.previewUrl + '&preview=true',
'_blank'
);
}
});
},
/**
* Track dirty state
*/
initDirtyTracking: function() {
const form = $('#bd-data-entry-form');
form.on('change input', 'input, select, textarea', function() {
BDDataEntry.isDirty = true;
form.data('saved', false);
});
// Also track on keyup for immediate feedback
form.on('keyup', 'input, textarea', function() {
BDDataEntry.isDirty = true;
form.data('saved', false);
});
},
/**
* Show status message
*/
showStatus: function(message, type) {
// Remove any existing messages
$('.bd-status-message').remove();
const $status = $('<div class="bd-status-message ' + type + '">' + message + '</div>');
$('.wrap h1').after($status);
$status.fadeIn();
setTimeout(function() {
$status.fadeOut(function() {
$(this).remove();
});
}, 5000);
}
};
// Initialize on DOM ready
$(document).ready(function() {
if ($('#bd-data-entry-form').length) {
BDDataEntry.init();
}
// Also handle successful form submission
$(document).ajaxComplete(function(event, xhr, settings) {
if (settings.url === ajaxurl && xhr.responseJSON && xhr.responseJSON.success) {
// Form was saved successfully
$('.bd-autosave-indicator').removeClass('saving').addClass('saved visible').text('Saved successfully!');
BDDataEntry.isDirty = false;
setTimeout(function() {
$('.bd-autosave-indicator').removeClass('visible saved');
}, 3000);
}
});
});
})(jQuery);