|
1 | | -/* global jQuery, BCCImageOCRWithOpenAI */ |
| 1 | +/* global jQuery, BCCImageOCRWithOpenAI, wp */ |
2 | 2 | (function ($) { |
3 | 3 | 'use strict'; |
4 | 4 |
|
5 | | - const SELECTORS = { |
6 | | - editImageBtn: '.wp_attachment_image .button, #imgedit-open-btn-' + BCCImageOCRWithOpenAI.attachment_id, |
| 5 | + const BUTTON_CLASS = 'bcc-openai-ocr-button'; |
| 6 | + |
| 7 | + // Selectors for the classic attachment edit screen (post.php?post=…&action=edit). |
| 8 | + const EDIT_SCREEN = { |
7 | 9 | altInput: '#attachment_alt', |
8 | 10 | captionInput: '#attachment_caption', |
9 | 11 | descTextarea: '#attachment_content' |
10 | 12 | }; |
11 | 13 |
|
| 14 | + // Selectors used inside the Media Library / Attachment Details modal. |
| 15 | + const MODAL = { |
| 16 | + root: '.media-modal', |
| 17 | + details: '.attachment-details', |
| 18 | + actions: '.attachment-actions', |
| 19 | + altInput: '[data-setting="alt"] input, [data-setting="alt"] textarea', |
| 20 | + captionTa: '[data-setting="caption"] textarea, [data-setting="caption"] input', |
| 21 | + descTa: '[data-setting="description"] textarea, [data-setting="description"] input' |
| 22 | + }; |
| 23 | + |
12 | 24 | function createButton() { |
13 | 25 | return $('<button>', { |
14 | 26 | type: 'button', |
15 | | - id: 'openai-ocr-button', |
16 | | - class: 'button button-primary', |
| 27 | + class: 'button button-primary ' + BUTTON_CLASS, |
17 | 28 | text: BCCImageOCRWithOpenAI.i18n.button, |
18 | 29 | css: { marginLeft: '6px' } |
19 | 30 | }); |
20 | 31 | } |
21 | 32 |
|
22 | 33 | function setFieldValue($field, value) { |
23 | | - if (!$field.length) { |
24 | | - return; |
25 | | - } |
| 34 | + if (!$field.length) return; |
26 | 35 | $field.val(value).trigger('change').trigger('input'); |
27 | 36 | } |
28 | 37 |
|
29 | | - function fillFields(data) { |
30 | | - setFieldValue($(SELECTORS.descTextarea), data.description || ''); |
31 | | - setFieldValue($(SELECTORS.altInput), data.alt_text || ''); |
32 | | - setFieldValue($(SELECTORS.captionInput), data.caption || ''); |
| 38 | + function getContextFor($btn) { |
| 39 | + const $details = $btn.closest(MODAL.details); |
| 40 | + if ($details.length) { |
| 41 | + return { kind: 'modal', $details: $details }; |
| 42 | + } |
| 43 | + return { kind: 'edit' }; |
33 | 44 | } |
34 | 45 |
|
35 | | - function fieldsHaveContent() { |
36 | | - return [SELECTORS.descTextarea, SELECTORS.altInput, SELECTORS.captionInput] |
37 | | - .some(function (sel) { |
38 | | - const $f = $(sel); |
39 | | - return $f.length && $.trim($f.val() || '') !== ''; |
40 | | - }); |
| 46 | + function getAttachmentId(ctx) { |
| 47 | + if (ctx.kind === 'modal') { |
| 48 | + // Most reliable: the attachment-details element carries data-id. |
| 49 | + let id = parseInt(ctx.$details.attr('data-id'), 10); |
| 50 | + if (id) return id; |
| 51 | + |
| 52 | + // Fallback: ask the active media frame for the current selection. |
| 53 | + try { |
| 54 | + if (window.wp && wp.media && wp.media.frame) { |
| 55 | + const sel = wp.media.frame.state() && wp.media.frame.state().get('selection'); |
| 56 | + if (sel && sel.first()) { |
| 57 | + id = parseInt(sel.first().id, 10); |
| 58 | + if (id) return id; |
| 59 | + } |
| 60 | + } |
| 61 | + } catch (e) { /* ignore */ } |
| 62 | + |
| 63 | + // Fallback: the Media Library uses ?item=<id> in the URL when an |
| 64 | + // attachment is opened in the modal (upload.php?item=123). |
| 65 | + try { |
| 66 | + const params = new URL(window.location.href).searchParams; |
| 67 | + const item = parseInt(params.get('item') || '', 10); |
| 68 | + if (item) return item; |
| 69 | + } catch (e) { /* ignore */ } |
| 70 | + |
| 71 | + // Fallback: hash often contains "#?item=123" while the modal is open. |
| 72 | + try { |
| 73 | + const hash = (window.location.hash || '').replace(/^#/, ''); |
| 74 | + const m = hash.match(/(?:^|[?&])item=(\d+)/); |
| 75 | + if (m) { |
| 76 | + const hashItem = parseInt(m[1], 10); |
| 77 | + if (hashItem) return hashItem; |
| 78 | + } |
| 79 | + } catch (e) { /* ignore */ } |
| 80 | + |
| 81 | + // Last resort: currently selected thumbnail in the grid. |
| 82 | + const selectedId = parseInt($('.media-modal .attachments .attachment.selected, .media-modal .attachments .attachment.details').first().attr('data-id') || '', 10); |
| 83 | + if (selectedId) return selectedId; |
| 84 | + |
| 85 | + return 0; |
| 86 | + } |
| 87 | + |
| 88 | + return parseInt(BCCImageOCRWithOpenAI.attachment_id, 10) || 0; |
41 | 89 | } |
42 | 90 |
|
43 | | - function run($btn) { |
44 | | - if (fieldsHaveContent() && !window.confirm(BCCImageOCRWithOpenAI.i18n.confirm_over)) { |
45 | | - return; |
| 91 | + function getFieldsFor(ctx) { |
| 92 | + if (ctx.kind === 'modal') { |
| 93 | + return { |
| 94 | + $desc: ctx.$details.find(MODAL.descTa).first(), |
| 95 | + $alt: ctx.$details.find(MODAL.altInput).first(), |
| 96 | + $caption: ctx.$details.find(MODAL.captionTa).first() |
| 97 | + }; |
46 | 98 | } |
| 99 | + return { |
| 100 | + $desc: $(EDIT_SCREEN.descTextarea), |
| 101 | + $alt: $(EDIT_SCREEN.altInput), |
| 102 | + $caption: $(EDIT_SCREEN.captionInput) |
| 103 | + }; |
| 104 | + } |
47 | 105 |
|
48 | | - const originalLabel = $btn.text(); |
49 | | - $btn.prop('disabled', true).text(BCCImageOCRWithOpenAI.i18n.working); |
| 106 | + function fieldsHaveContent(fields) { |
| 107 | + return [fields.$desc, fields.$alt, fields.$caption].some(function ($f) { |
| 108 | + return $f && $f.length && $.trim($f.val() || '') !== ''; |
| 109 | + }); |
| 110 | + } |
| 111 | + |
| 112 | + function fillFields(ctx, fields, data) { |
| 113 | + setFieldValue(fields.$desc, data.description || ''); |
| 114 | + setFieldValue(fields.$alt, data.alt_text || ''); |
| 115 | + setFieldValue(fields.$caption, data.caption || ''); |
50 | 116 |
|
51 | | - // Detect the WPML / Polylang language selected in the admin top-bar. |
52 | | - // 1. The current admin URL usually carries ?lang=xx. |
53 | | - // 2. Otherwise look for the WPML admin language switcher in the top-bar. |
| 117 | + // In modal context, also push values into the Backbone model so the |
| 118 | + // modal's own auto-save (attachment.save()) persists the change and |
| 119 | + // the user does not need to click anything else. |
| 120 | + if (ctx.kind === 'modal') { |
| 121 | + try { |
| 122 | + const id = getAttachmentId(ctx); |
| 123 | + if (id && window.wp && wp.media && wp.media.attachment) { |
| 124 | + const model = wp.media.attachment(id); |
| 125 | + const attrs = { |
| 126 | + alt: data.alt_text || '', |
| 127 | + caption: data.caption || '', |
| 128 | + description: data.description || '' |
| 129 | + }; |
| 130 | + model.set(attrs); |
| 131 | + if (typeof model.save === 'function') { |
| 132 | + model.save(attrs); |
| 133 | + } |
| 134 | + } |
| 135 | + } catch (e) { /* ignore – server-side save already happened */ } |
| 136 | + } |
| 137 | + } |
| 138 | + |
| 139 | + function detectLang() { |
| 140 | + // 1) URL ?lang= takes precedence (matches WPML/Polylang admin top-bar). |
54 | 141 | let lang = ''; |
55 | 142 | try { |
56 | 143 | lang = new URL(window.location.href).searchParams.get('lang') || ''; |
57 | 144 | } catch (e) { /* ignore */ } |
58 | | - if (!lang) { |
59 | | - const $wpmlActive = jQuery('#wp-admin-bar-WPML_ALS .ab-item').first(); |
60 | | - if ($wpmlActive.length) { |
61 | | - lang = ($wpmlActive.attr('href') || '').split('lang=')[1] || ''; |
62 | | - lang = lang.split('&')[0]; |
| 145 | + if (lang && lang !== 'all') return lang; |
| 146 | + |
| 147 | + // 2) Server-side resolved value (WPML/Polylang/locale). |
| 148 | + if (BCCImageOCRWithOpenAI.current_lang) { |
| 149 | + return String(BCCImageOCRWithOpenAI.current_lang); |
| 150 | + } |
| 151 | + |
| 152 | + // 3) WPML's icl_vars (loaded by the sitepress-js-extra script). |
| 153 | + try { |
| 154 | + if (typeof window.icl_vars === 'object' && window.icl_vars && window.icl_vars.current_language) { |
| 155 | + return String(window.icl_vars.current_language); |
63 | 156 | } |
| 157 | + } catch (e) { /* ignore */ } |
| 158 | + |
| 159 | + // 4) WPML admin language switcher in the top-bar. |
| 160 | + const $wpml = $('#wp-admin-bar-WPML_ALS .ab-item').first(); |
| 161 | + if ($wpml.length) { |
| 162 | + const href = $wpml.attr('href') || ''; |
| 163 | + const fromHref = (href.split('lang=')[1] || '').split('&')[0]; |
| 164 | + if (fromHref) return fromHref; |
64 | 165 | } |
65 | 166 |
|
| 167 | + return ''; |
| 168 | + } |
| 169 | + |
| 170 | + function run($btn) { |
| 171 | + const ctx = getContextFor($btn); |
| 172 | + const attachmentId = getAttachmentId(ctx); |
| 173 | + if (!attachmentId) { |
| 174 | + window.alert(BCCImageOCRWithOpenAI.i18n.failed + ' ' + BCCImageOCRWithOpenAI.i18n.no_id); |
| 175 | + return; |
| 176 | + } |
| 177 | + |
| 178 | + const fields = getFieldsFor(ctx); |
| 179 | + if (fieldsHaveContent(fields) && !window.confirm(BCCImageOCRWithOpenAI.i18n.confirm_over)) { |
| 180 | + return; |
| 181 | + } |
| 182 | + |
| 183 | + const originalLabel = $btn.text(); |
| 184 | + $btn.prop('disabled', true).text(BCCImageOCRWithOpenAI.i18n.working); |
| 185 | + |
66 | 186 | $.post(BCCImageOCRWithOpenAI.ajax_url, { |
67 | 187 | action: BCCImageOCRWithOpenAI.action, |
68 | 188 | nonce: BCCImageOCRWithOpenAI.nonce, |
69 | | - attachment_id: BCCImageOCRWithOpenAI.attachment_id, |
70 | | - lang: lang |
| 189 | + attachment_id: attachmentId, |
| 190 | + lang: detectLang() |
71 | 191 | }) |
72 | 192 | .done(function (response) { |
73 | 193 | if (response && response.success) { |
74 | | - fillFields(response.data || {}); |
| 194 | + fillFields(ctx, fields, response.data || {}); |
75 | 195 | $btn.text(BCCImageOCRWithOpenAI.i18n.done); |
76 | 196 | window.setTimeout(function () { |
77 | 197 | $btn.prop('disabled', false).text(originalLabel); |
|
89 | 209 | }); |
90 | 210 | } |
91 | 211 |
|
92 | | - function insertButton() { |
93 | | - if ($('#openai-ocr-button').length) { |
| 212 | + // ---- Insertion: classic attachment edit screen ---------------------------- |
| 213 | + |
| 214 | + function insertButtonOnEditScreen() { |
| 215 | + // Don't insert twice on this screen. |
| 216 | + if ($('.wp_attachment_image').find('.' + BUTTON_CLASS).length || |
| 217 | + $('#wpbody-content').find('> .' + BUTTON_CLASS).length) { |
94 | 218 | return true; |
95 | 219 | } |
96 | 220 |
|
97 | | - // Preferred location: directly after the "Edit Image" button under the thumbnail. |
98 | | - const $editBtn = $('#imgedit-open-btn-' + BCCImageOCRWithOpenAI.attachment_id); |
| 221 | + const id = parseInt(BCCImageOCRWithOpenAI.attachment_id, 10) || 0; |
| 222 | + const $editBtn = id ? $('#imgedit-open-btn-' + id) : $(); |
| 223 | + |
99 | 224 | if ($editBtn.length) { |
100 | 225 | const $btn = createButton(); |
101 | 226 | $editBtn.after($btn); |
102 | 227 | $btn.on('click', function (e) { e.preventDefault(); run($btn); }); |
103 | 228 | return true; |
104 | 229 | } |
105 | 230 |
|
106 | | - // Fallback: above the description textarea. |
107 | | - const $desc = $(SELECTORS.descTextarea); |
| 231 | + const $desc = $(EDIT_SCREEN.descTextarea); |
108 | 232 | if ($desc.length) { |
109 | 233 | const $btn = createButton().css({ marginLeft: 0, marginBottom: '6px', display: 'inline-block' }); |
110 | 234 | $desc.before($btn); |
|
115 | 239 | return false; |
116 | 240 | } |
117 | 241 |
|
| 242 | + // ---- Insertion: media library / attachment details modal ----------------- |
| 243 | + |
| 244 | + function insertButtonInModal($details) { |
| 245 | + if (!$details || !$details.length) return false; |
| 246 | + if ($details.find('.' + BUTTON_CLASS).length) return true; |
| 247 | + |
| 248 | + const $actions = $details.find(MODAL.actions).first(); |
| 249 | + if (!$actions.length) return false; |
| 250 | + |
| 251 | + // Only show for images. |
| 252 | + const dataType = ($details.attr('data-type') || '').toLowerCase(); |
| 253 | + if (dataType && dataType !== 'image') return false; |
| 254 | + |
| 255 | + const $btn = createButton(); |
| 256 | + $actions.append($btn); |
| 257 | + $btn.on('click', function (e) { e.preventDefault(); run($btn); }); |
| 258 | + return true; |
| 259 | + } |
| 260 | + |
| 261 | + function refreshModalButtons() { |
| 262 | + $('.media-modal').find(MODAL.details).each(function () { |
| 263 | + insertButtonInModal($(this)); |
| 264 | + }); |
| 265 | + } |
| 266 | + |
| 267 | + // ---- Bootstrap ----------------------------------------------------------- |
| 268 | + |
118 | 269 | $(function () { |
119 | | - if (insertButton()) { |
120 | | - return; |
| 270 | + // Edit screen: try once, then briefly retry for late DOM. |
| 271 | + if (!insertButtonOnEditScreen()) { |
| 272 | + let attempts = 0; |
| 273 | + const interval = window.setInterval(function () { |
| 274 | + attempts += 1; |
| 275 | + if (insertButtonOnEditScreen() || attempts > 30) { |
| 276 | + window.clearInterval(interval); |
| 277 | + } |
| 278 | + }, 250); |
| 279 | + } |
| 280 | + |
| 281 | + // Modal: observe the body for the modal's attachment-details panel and |
| 282 | + // refresh on selection changes. |
| 283 | + refreshModalButtons(); |
| 284 | + |
| 285 | + // Re-check whenever a media item is clicked or the modal content updates. |
| 286 | + $(document).on('click', '.media-modal .attachments .attachment, .media-modal .media-button', function () { |
| 287 | + window.setTimeout(refreshModalButtons, 30); |
| 288 | + }); |
| 289 | + |
| 290 | + // MutationObserver: catch frame swaps (Upload Files vs. Media Library tabs, |
| 291 | + // next/previous arrows, etc.) without polling forever. |
| 292 | + if (typeof MutationObserver !== 'undefined') { |
| 293 | + const observer = new MutationObserver(function () { |
| 294 | + refreshModalButtons(); |
| 295 | + }); |
| 296 | + $('body').each(function () { |
| 297 | + observer.observe(this, { childList: true, subtree: true }); |
| 298 | + }); |
121 | 299 | } |
122 | | - // Some elements load asynchronously; retry briefly. |
123 | | - let attempts = 0; |
124 | | - const interval = window.setInterval(function () { |
125 | | - attempts += 1; |
126 | | - if (insertButton() || attempts > 30) { |
127 | | - window.clearInterval(interval); |
128 | | - } |
129 | | - }, 250); |
130 | 300 | }); |
131 | 301 |
|
132 | 302 | })(jQuery); |
0 commit comments