| @@ -47,41 +47,119 @@ | ||
| 47 | 47 | /** |
| 48 | 48 | * Enqueue stylesheets |
| 49 | 49 | */ |
| 50 | 50 | private function enqueueStyles() { |
| 51 | - wp_enqueue_style( | |
| 52 | - 'easy-invoice-tailwind', | |
| 53 | - 'https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css', | |
| 54 | - array(), | |
| 55 | - EASY_INVOICE_VERSION | |
| 56 | - ); | |
| 57 | - // Main admin styles | |
| 58 | - wp_enqueue_style( | |
| 59 | - 'easy-invoice-builder', | |
| 60 | - EASY_INVOICE_PLUGIN_URL . 'assets/css/easy-invoice.css', | |
| 61 | - array(), | |
| 62 | - EASY_INVOICE_VERSION | |
| 63 | - ); | |
| 64 | - wp_enqueue_style( | |
| 65 | - 'easy-invoice-preview', | |
| 66 | - EASY_INVOICE_PLUGIN_URL . 'assets/css/invoice-preview.css', | |
| 67 | - array(), | |
| 68 | - EASY_INVOICE_VERSION | |
| 69 | - ); | |
| 70 | 51 | |
| 71 | - // Add additional admin styles based on screen | |
| 52 | + // Tailwind: the purged ~47 KB build is the default; the full 2.8 MB | |
| 53 | + // dump stays on disk as the fallback. | |
| 54 | + // | |
| 55 | + // The purged file is the shipped v2 bundle with unused rules removed -- | |
| 56 | + // NOT a v3 rebuild, which would rename utilities and change the palette. | |
| 57 | + // Every rule it keeps is byte-identical to the full file, so anything | |
| 58 | + // still on a page renders exactly as before. It was verified by | |
| 59 | + // pixel-diffing 41 admin screens (every core page, every addon page, | |
| 60 | + // three breakpoints, the drawer, a modal and toasts) against the full | |
| 61 | + // file: zero differing pixels. See tools/tailwind-build/README.md. | |
| 62 | + // | |
| 63 | + // A site that adds markup using utilities the plugin never uses -- | |
| 64 | + // a custom template, a child theme -- can go back to the full file: | |
| 65 | + // update_option('easy_invoice_tailwind_purged', '0'); | |
| 66 | + // or | |
| 67 | + // add_filter('easy_invoice_use_purged_tailwind', '__return_false'); | |
| 68 | + // | |
| 69 | + // The option was previously opt-IN ('1' enabled the purged file). It is | |
| 70 | + // now opt-OUT: only an explicit '0' disables it. A site that had set | |
| 71 | + // '1' keeps working; a site that never set it gets the win. | |
| 72 | + // | |
| 73 | + // The file_exists check below means a deployment that lost the purged | |
| 74 | + // file falls back to the full one rather than 404-ing the stylesheet. | |
| 75 | + $purged_option = get_option('easy_invoice_tailwind_purged', '1'); | |
| 76 | + $use_purged = apply_filters( | |
| 77 | + 'easy_invoice_use_purged_tailwind', | |
| 78 | + '0' !== (string) $purged_option && false !== $purged_option | |
| 79 | + ); | |
| 80 | + $purged_rel = 'assets/lib/tailwind/tailwind.purged.min.css'; | |
| 81 | + $shipped_rel = 'assets/lib/tailwind/tailwind.min.css'; | |
| 82 | + $tailwind_rel = ($use_purged && file_exists(EASY_INVOICE_PLUGIN_DIR . $purged_rel)) | |
| 83 | + ? $purged_rel | |
| 84 | + : $shipped_rel; | |
| 72 | 85 | |
| 86 | + wp_enqueue_style( | |
| 87 | + 'easy-invoice-tailwind', | |
| 88 | + EASY_INVOICE_PLUGIN_URL . $tailwind_rel, | |
| 89 | + array(), | |
| 90 | + EASY_INVOICE_VERSION | |
| 91 | + ); | |
| 73 | 92 | |
| 74 | - // Add Font Awesome for icons | |
| 75 | - wp_enqueue_style( | |
| 76 | - 'font-awesome', | |
| 77 | - 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css', | |
| 78 | - array(), | |
| 79 | - '6.0.0' | |
| 80 | - ); | |
| 93 | + // Colour names the views use that Tailwind v2 never had (amber, emerald, | |
| 94 | + // teal …) — without these every amber notice and emerald badge was unstyled. | |
| 95 | + wp_enqueue_style( | |
| 96 | + 'easy-invoice-palette-ext', | |
| 97 | + EASY_INVOICE_PLUGIN_URL . 'assets/css/palette-ext.css', | |
| 98 | + array( 'easy-invoice-tailwind' ), | |
| 99 | + EASY_INVOICE_VERSION | |
| 100 | + ); | |
| 81 | 101 | |
| 82 | - // Add RTL support | |
| 83 | - wp_style_add_data('easy-invoice-admin', 'rtl', 'replace'); | |
| 102 | + $page = isset($_GET['page']) ? sanitize_text_field($_GET['page']) : ''; | |
| 103 | + | |
| 104 | + // Main plugin layout + header strip alignment (always on Easy Invoice admin screens). | |
| 105 | + wp_enqueue_style( | |
| 106 | + 'easy-invoice-main', | |
| 107 | + EASY_INVOICE_PLUGIN_URL . 'assets/css/easy-invoice.css', | |
| 108 | + array(), | |
| 109 | + EASY_INVOICE_VERSION | |
| 110 | + ); | |
| 111 | + | |
| 112 | + if($page === 'easy-invoice-builder') { | |
| 113 | + // Main admin styles | |
| 114 | + | |
| 115 | + wp_enqueue_style( | |
| 116 | + 'easy-invoice-preview', | |
| 117 | + EASY_INVOICE_PLUGIN_URL . 'assets/css/invoice-preview.css', | |
| 118 | + array(), | |
| 119 | + EASY_INVOICE_VERSION | |
| 120 | + ); | |
| 121 | + } | |
| 122 | + | |
| 123 | + // Font Awesome (~87 KB) is used by the heavyweight templates | |
| 124 | + // (clients, settings, reports, invoices, quotes, payments) but | |
| 125 | + // NOT by the lightweight admin pages — Dashboard, Addons grid, | |
| 126 | + // License, Free-vs-Pro, Join Community, and every addon-settings | |
| 127 | + // sub-page (those use dashicons). Skip enqueueing FA on the | |
| 128 | + // deny-listed pages to save the bandwidth and the parse cost. | |
| 129 | + // Filterable so site-specific custom templates can opt back in. | |
| 130 | + $fa_deny = (array) apply_filters('easy_invoice_font_awesome_skip_pages', [ | |
| 131 | + 'easy-invoice-dashboard', | |
| 132 | + 'easy-invoice-addons', | |
| 133 | + 'easy-invoice-license', | |
| 134 | + 'easy-invoice-free-vs-pro', | |
| 135 | + 'easy-invoice-join-community', | |
| 136 | + ]); | |
| 137 | + $is_addon_subpage = ($page !== '' && strpos($page, 'easy-invoice-addon-') === 0); | |
| 138 | + if (!in_array($page, $fa_deny, true) && !$is_addon_subpage) { | |
| 139 | + wp_enqueue_style( | |
| 140 | + 'font-awesome', | |
| 141 | + EASY_INVOICE_PLUGIN_URL . 'assets/lib/font-awesome/css/all.min.css', | |
| 142 | + array(), | |
| 143 | + EASY_INVOICE_VERSION | |
| 144 | + ); | |
| 145 | + } | |
| 146 | + | |
| 147 | + | |
| 148 | + // RTL support. | |
| 149 | + // | |
| 150 | + // Strategy: 'append' (not 'replace') because Tailwind utility | |
| 151 | + // classes generated into easy-invoice.css remain valid LTR — only | |
| 152 | + // the small set of custom directional rules (sidebar borders, | |
| 153 | + // page-header negative margins, drawer offsets) need mirroring. | |
| 154 | + // WordPress loads assets/css/easy-invoice-rtl.css AFTER | |
| 155 | + // easy-invoice.css when is_rtl() is true; that file holds only the | |
| 156 | + // directional overrides. | |
| 157 | + // | |
| 158 | + // 'easy-invoice-main' is the handle of the file we want to extend | |
| 159 | + // (line 87 above). The previous handle 'easy-invoice-admin' did | |
| 160 | + // not exist and so the call was a no-op. | |
| 161 | + wp_style_add_data('easy-invoice-main', 'rtl', 'append'); | |
| 84 | 162 | } |
| 85 | 163 | |
| 86 | 164 | /** |
| 87 | 165 | * Enqueue scripts |
| @@ -107,12 +185,27 @@ | ||
| 107 | 185 | wp_enqueue_script('wp-hooks'); |
| 108 | 186 | |
| 109 | 187 | // Settings page script |
| 110 | 188 | if (isset($_GET['page']) && $_GET['page'] === 'easy-invoice-settings') { |
| 189 | + // settings.js calls .sortable() and .disableSelection() on the | |
| 190 | + // payment-gateway list, so jQuery UI Sortable must be present and | |
| 191 | + // printed first. SettingsController::enqueueAdminScripts() also | |
| 192 | + // registers this same handle; WordPress keeps whichever call runs | |
| 193 | + // first and silently drops the other's dependency array, and this | |
| 194 | + // one runs first (AdminAssets is wired up during EasyInvoice::init, | |
| 195 | + // before SettingsController::init). Declaring the jQuery UI handles | |
| 196 | + // here lets wp_scripts resolve the order rather than leaving it to | |
| 197 | + // the sequence the two enqueue callbacks happen to fire in. | |
| 198 | + // | |
| 199 | + // Only core handles are listed. select2 / easy-invoice-toast are | |
| 200 | + // also used by settings.js but are registered by SettingsController | |
| 201 | + // later in the request; naming them here would mean settings.js is | |
| 202 | + // dropped entirely if that callback ever stops running, so they are | |
| 203 | + // deliberately left out. | |
| 111 | 204 | wp_enqueue_script( |
| 112 | 205 | 'easy-invoice-settings', |
| 113 | 206 | EASY_INVOICE_PLUGIN_URL . 'assets/js/settings.js', |
| 114 | - array('jquery', 'wp-util', 'wp-api-fetch', 'wp-i18n'), | |
| 207 | + array('jquery', 'jquery-ui-core', 'jquery-ui-sortable', 'wp-util', 'wp-api-fetch', 'wp-i18n'), | |
| 115 | 208 | EASY_INVOICE_VERSION, |
| 116 | 209 | true |
| 117 | 210 | ); |
| 118 | 211 | |
| @@ -160,10 +253,46 @@ | ||
| 160 | 253 | // Toast notification system - global across the plugin |
| 161 | 254 | wp_register_script('easy-invoice-toast', EASY_INVOICE_PLUGIN_URL . 'assets/js/easy-invoice-toast.js', array('jquery', 'wp-api-fetch', 'wp-i18n'), EASY_INVOICE_VERSION, true); |
| 162 | 255 | wp_enqueue_script('easy-invoice-toast'); |
| 163 | 256 | |
| 164 | - // jsPDF for PDF generation - available on all Easy Invoice pages | |
| 165 | - wp_register_script('jspdf', 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js', array(), '2.5.1', true); | |
| 257 | + // Bulk "Send Email" teaser — only on the invoice / quote listings. | |
| 258 | + // Always injects the option (even without Pro) so users discover the | |
| 259 | + // feature; when Pro is inactive, clicking Apply opens the upgrade | |
| 260 | + // dialog instead of doing the work. | |
| 261 | + $current_page = isset($_GET['page']) ? sanitize_key($_GET['page']) : ''; | |
| 262 | + if (in_array($current_page, ['easy-invoice-all', 'easy-quote-all'], true)) { | |
| 263 | + wp_register_script( | |
| 264 | + 'easy-invoice-bulk-send-email-teaser', | |
| 265 | + EASY_INVOICE_PLUGIN_URL . 'assets/js/bulk-send-email-teaser.js', | |
| 266 | + array('jquery', 'easy-invoice-confirmation-modal', 'easy-invoice-toast'), | |
| 267 | + EASY_INVOICE_VERSION, | |
| 268 | + true | |
| 269 | + ); | |
| 270 | + wp_localize_script('easy-invoice-bulk-send-email-teaser', 'easyInvoiceBulkSendTeaser', array( | |
| 271 | + 'hasPro' => function_exists('easy_invoice_has_pro') && easy_invoice_has_pro(), | |
| 272 | + 'upgradeUrl' => 'https://matrixaddons.com/plugins/easy-invoice/#pricing', | |
| 273 | + 'i18n' => array( | |
| 274 | + // Send Email | |
| 275 | + 'send_email_label' => __('Send Email (Pro)', 'easy-invoice'), | |
| 276 | + 'send_email_feature_name' => __('Bulk Send Email', 'easy-invoice'), | |
| 277 | + 'send_email_feature_description'=> __('Select multiple invoices or quotes and send the configured "Available" email to every selected document\'s client in a single click. Includes a per-row success / failure report so you can spot deliverability problems immediately.', 'easy-invoice'), | |
| 278 | + // Export | |
| 279 | + 'export_feature_name' => __('Bulk Export Selected', 'easy-invoice'), | |
| 280 | + 'export_feature_description' => __('Export your selected invoices or quotes to a clean, accounting-ready CSV — perfect for QuickBooks, Xero, audit trails, or migrating to a new system.', 'easy-invoice'), | |
| 281 | + // Generic | |
| 282 | + 'upgrade_required' => __('This action requires Easy Invoice Pro.', 'easy-invoice'), | |
| 283 | + ), | |
| 284 | + )); | |
| 285 | + wp_enqueue_script('easy-invoice-bulk-send-email-teaser'); | |
| 286 | + } | |
| 287 | + | |
| 288 | + // jsPDF + html2canvas for PDF generation - available on all Easy Invoice pages. | |
| 289 | + // Both are bundled in assets/js/vendors/ rather than loaded from a CDN: | |
| 290 | + // WordPress.org requires every asset to ship inside the plugin, and a remote | |
| 291 | + // CDN is a hard runtime dependency for a core feature. | |
| 292 | + wp_register_script('html2canvas', EASY_INVOICE_PLUGIN_URL . 'assets/js/vendors/html2canvas.min.js', array(), '1.4.1', true); | |
| 293 | + wp_register_script('jspdf', EASY_INVOICE_PLUGIN_URL . 'assets/js/vendors/jspdf.umd.min.js', array(), '2.5.1', true); | |
| 294 | + wp_enqueue_script('html2canvas'); | |
| 166 | 295 | wp_enqueue_script('jspdf'); |
| 167 | 296 | |
| 168 | 297 | |
| 169 | 298 | // Conditionally load invoice-specific scripts only on invoice pages |
| @@ -176,13 +305,36 @@ | ||
| 176 | 305 | wp_register_script('easy-invoice-save', EASY_INVOICE_PLUGIN_URL . 'assets/js/invoice-save.js', array('jquery', 'easy-invoice-scripts', 'easy-invoice-payment-manager', 'wp-api-fetch', 'wp-i18n'), EASY_INVOICE_VERSION, true); |
| 177 | 306 | wp_enqueue_script('easy-invoice-save'); |
| 178 | 307 | } |
| 179 | 308 | |
| 180 | - // Conditionally load quote-specific scripts only on quote pages | |
| 181 | - if (isset($_GET['page']) && $_GET['page'] === 'easy-invoice-quotes' && isset($_GET['action']) && $_GET['action'] === 'edit') { | |
| 182 | - // Quote builder scripts - these are loaded directly in the quote builder template | |
| 183 | - // to avoid conflicts with invoice scripts | |
| 309 | + // Quote builder (full-screen editor) | |
| 310 | + if (isset($_GET['page']) && $_GET['page'] === 'easy-invoice-quote-builder') { | |
| 311 | + wp_register_script( | |
| 312 | + 'easy-quote-save', | |
| 313 | + EASY_INVOICE_PLUGIN_URL . 'assets/js/quote-save.js', | |
| 314 | + array('jquery', 'easy-invoice-scripts', 'easy-invoice-payment-manager', 'wp-api-fetch', 'wp-i18n'), | |
| 315 | + EASY_INVOICE_VERSION, | |
| 316 | + true | |
| 317 | + ); | |
| 318 | + wp_enqueue_script('easy-quote-save'); | |
| 184 | 319 | } |
| 320 | + | |
| 321 | + if (isset($_GET['page']) && in_array($_GET['page'], array('easy-invoice-builder', 'easy-invoice-quote-builder'), true)) { | |
| 322 | + wp_enqueue_script( | |
| 323 | + 'easy-invoice-builder-mobile-tabs', | |
| 324 | + EASY_INVOICE_PLUGIN_URL . 'assets/js/builder-mobile-tabs.js', | |
| 325 | + array('jquery'), | |
| 326 | + EASY_INVOICE_VERSION, | |
| 327 | + true | |
| 328 | + ); | |
| 329 | + wp_enqueue_script( | |
| 330 | + 'easy-invoice-builder-ux', | |
| 331 | + EASY_INVOICE_PLUGIN_URL . 'assets/js/builder-ux.js', | |
| 332 | + array('jquery', 'easy-invoice-scripts'), | |
| 333 | + EASY_INVOICE_VERSION, | |
| 334 | + true | |
| 335 | + ); | |
| 336 | + } | |
| 185 | 337 | } |
| 186 | 338 | |
| 187 | 339 | /** |
| 188 | 340 | * Localize script data |
| @@ -198,8 +350,10 @@ | ||
| 198 | 350 | $currency_code = $settings['easy_invoice_currency_code'] ?? 'USD'; |
| 199 | 351 | $currency_position = $settings['easy_invoice_currency_position'] ?? 'left'; |
| 200 | 352 | $currency_symbol = easy_invoice_get_currency_symbol(); |
| 201 | 353 | |
| 354 | + $current_admin_page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : ''; | |
| 355 | + | |
| 202 | 356 | // Default data |
| 203 | 357 | $script_data = array( |
| 204 | 358 | 'ajaxUrl' => admin_url('admin-ajax.php'), |
| 205 | 359 | 'nonce' => wp_create_nonce('easy_invoice_nonce'), |
| @@ -215,8 +369,49 @@ | ||
| 215 | 369 | 'add_item' => __('Add Item', 'easy-invoice'), |
| 216 | 370 | 'delete_item' => __('Delete Item', 'easy-invoice'), |
| 217 | 371 | 'error' => __('An error occurred', 'easy-invoice'), |
| 218 | 372 | ), |
| 373 | + ); | |
| 374 | + | |
| 375 | + if (in_array($current_admin_page, array('easy-invoice-builder', 'easy-invoice-quote-builder'), true)) { | |
| 376 | + $script_data['i18n'] = array_merge( | |
| 377 | + $script_data['i18n'], | |
| 378 | + array( | |
| 379 | + 'saving' => __('Saving…', 'easy-invoice'), | |
| 380 | + 'sending' => __('Sending…', 'easy-invoice'), | |
| 381 | + 'save_invoice' => __('Save Invoice', 'easy-invoice'), | |
| 382 | + 'update_invoice' => __('Update Invoice', 'easy-invoice'), | |
| 383 | + 'save_quote' => __('Save Quote', 'easy-invoice'), | |
| 384 | + 'update_quote' => __('Update Quote', 'easy-invoice'), | |
| 385 | + 'send_invoice' => __('Send Invoice', 'easy-invoice'), | |
| 386 | + 'send_quote' => __('Send Quote', 'easy-invoice'), | |
| 387 | + 'save_first_invoice' => __('Please save the invoice before sending.', 'easy-invoice'), | |
| 388 | + 'save_first_quote' => __('Please save the quote before sending.', 'easy-invoice'), | |
| 389 | + 'email_sent_invoice' => __('Invoice email sent successfully.', 'easy-invoice'), | |
| 390 | + 'email_sent_quote' => __('Quote email sent successfully.', 'easy-invoice'), | |
| 391 | + 'email_error' => __('Error sending email.', 'easy-invoice'), | |
| 392 | + 'network_error' => __('Error connecting to server.', 'easy-invoice'), | |
| 393 | + 'tab_editor' => __('Editor', 'easy-invoice'), | |
| 394 | + 'tab_preview' => __('Preview', 'easy-invoice'), | |
| 395 | + 'confirm_send_invoice_title' => __('Send invoice by email?', 'easy-invoice'), | |
| 396 | + 'confirm_send_invoice_message' => __('This will email the invoice to the client using your configured template.', 'easy-invoice'), | |
| 397 | + 'confirm_send_quote_title' => __('Send quote by email?', 'easy-invoice'), | |
| 398 | + 'confirm_send_quote_message' => __('This will email the quote to the client using your configured template.', 'easy-invoice'), | |
| 399 | + 'confirm_send_confirm' => __('Send', 'easy-invoice'), | |
| 400 | + 'confirm_cancel' => __('Cancel', 'easy-invoice'), | |
| 401 | + 'builder_unsaved_warning' => __('You have unsaved changes. If you leave this page, your changes may be lost.', 'easy-invoice'), | |
| 402 | + 'builder_title_hint' => __('The title above matches the document title field in the editor.', 'easy-invoice'), | |
| 403 | + 'builder_shell_hint' => __('On smaller screens, use the tabs to switch between the editor and the live preview.', 'easy-invoice'), | |
| 404 | + 'builder_shell_hint_dismiss' => __('Got it', 'easy-invoice'), | |
| 405 | + 'send_invoice_confirm_browser' => __('Send this invoice by email?', 'easy-invoice'), | |
| 406 | + 'send_quote_confirm_browser' => __('Send this quote by email?', 'easy-invoice'), | |
| 407 | + ) | |
| 408 | + ); | |
| 409 | + } | |
| 410 | + | |
| 411 | + $script_data = array_merge( | |
| 412 | + $script_data, | |
| 413 | + array( | |
| 219 | 414 | 'urls' => array( |
| 220 | 415 | 'preview' => admin_url('admin.php?action=easy_invoice_preview&invoice_id='), |
| 221 | 416 | 'edit' => admin_url('admin.php?page=easy-invoice-new&id='), |
| 222 | 417 | ), |
| @@ -228,8 +423,10 @@ | ||
| 228 | 423 | 'thousand_separator' => $settings['easy_invoice_thousands_separator'] ?? ',', |
| 229 | 424 | 'decimal_places' => $settings['easy_invoice_decimal_precision'] ?? 2, |
| 230 | 425 | 'date_format' => get_option('date_format', 'F j, Y'), |
| 231 | 426 | ), |
| 427 | + 'showAdjustField' => \EasyInvoice\Controllers\SettingsController::shouldShowInvoiceAdjustField(), | |
| 428 | + ) | |
| 232 | 429 | ); |
| 233 | 430 | |
| 234 | 431 | // Localize common scripts |
| 235 | 432 | wp_localize_script('easy-invoice-scripts', 'easyInvoice', $script_data); |
| @@ -304,9 +501,9 @@ | ||
| 304 | 501 | $default_items = [ |
| 305 | 502 | [ |
| 306 | 503 | 'name' => '', |
| 307 | 504 | 'description' => '', |
| 308 | - 'quantity' => 1, | |
| 505 | + 'quantity' => 0, | |
| 309 | 506 | 'price' => 0, |
| 310 | 507 | 'taxable' => true |
| 311 | 508 | ] |
| 312 | 509 | ]; |
| @@ -317,11 +514,11 @@ | ||
| 317 | 514 | wp_localize_script('easy-invoice-builder', 'easyInvoice', $script_data); |
| 318 | 515 | wp_localize_script('easy-invoice-save', 'easyInvoice', $script_data); |
| 319 | 516 | } |
| 320 | 517 | |
| 321 | - // Conditionally localize quote-specific scripts | |
| 322 | - if (isset($_GET['page']) && $_GET['page'] === 'easy-invoice-quotes' && isset($_GET['action']) && $_GET['action'] === 'edit') { | |
| 323 | - // Quote-specific data is handled in the quote builder template | |
| 324 | - // to avoid conflicts with invoice scripts | |
| 518 | + if (isset($_GET['page']) && $_GET['page'] === 'easy-invoice-quote-builder') { | |
| 519 | + // Override showAdjustField for quote pages | |
| 520 | + $script_data['showAdjustField'] = \EasyInvoice\Controllers\SettingsController::shouldShowQuoteAdjustField(); | |
| 521 | + wp_localize_script('easy-quote-save', 'easyInvoice', $script_data); | |
| 325 | 522 | } |
| 326 | 523 | } |
| 327 | 524 | } |