Skip to content

Commit 2516c09

Browse files
committed
Add OCR button on Media library modal and improve language detection
Make the admin media OCR button more robust across the attachment edit screen and the Media Library modal. JavaScript: add modal-aware selectors, reliably detect the current attachment ID, detect admin language from multiple sources, inject/update fields in both edit screen and modal (including Backbone model save), and observe DOM changes to insert buttons dynamically. PHP: add detect_admin_language() to resolve WPML/Polylang/URL/locale precedence, enqueue the script on upload and post editor screens, pass optional attachment_id and current_lang to the script, and add an i18n string for missing attachment ID. Overall improves UX and compatibility with multilingual setups and the media modal.
1 parent 1c75f23 commit 2516c09

2 files changed

Lines changed: 285 additions & 56 deletions

File tree

Lines changed: 220 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,197 @@
1-
/* global jQuery, BCCImageOCRWithOpenAI */
1+
/* global jQuery, BCCImageOCRWithOpenAI, wp */
22
(function ($) {
33
'use strict';
44

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 = {
79
altInput: '#attachment_alt',
810
captionInput: '#attachment_caption',
911
descTextarea: '#attachment_content'
1012
};
1113

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+
1224
function createButton() {
1325
return $('<button>', {
1426
type: 'button',
15-
id: 'openai-ocr-button',
16-
class: 'button button-primary',
27+
class: 'button button-primary ' + BUTTON_CLASS,
1728
text: BCCImageOCRWithOpenAI.i18n.button,
1829
css: { marginLeft: '6px' }
1930
});
2031
}
2132

2233
function setFieldValue($field, value) {
23-
if (!$field.length) {
24-
return;
25-
}
34+
if (!$field.length) return;
2635
$field.val(value).trigger('change').trigger('input');
2736
}
2837

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' };
3344
}
3445

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;
4189
}
4290

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+
};
4698
}
99+
return {
100+
$desc: $(EDIT_SCREEN.descTextarea),
101+
$alt: $(EDIT_SCREEN.altInput),
102+
$caption: $(EDIT_SCREEN.captionInput)
103+
};
104+
}
47105

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 || '');
50116

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).
54141
let lang = '';
55142
try {
56143
lang = new URL(window.location.href).searchParams.get('lang') || '';
57144
} 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);
63156
}
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;
64165
}
65166

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+
66186
$.post(BCCImageOCRWithOpenAI.ajax_url, {
67187
action: BCCImageOCRWithOpenAI.action,
68188
nonce: BCCImageOCRWithOpenAI.nonce,
69-
attachment_id: BCCImageOCRWithOpenAI.attachment_id,
70-
lang: lang
189+
attachment_id: attachmentId,
190+
lang: detectLang()
71191
})
72192
.done(function (response) {
73193
if (response && response.success) {
74-
fillFields(response.data || {});
194+
fillFields(ctx, fields, response.data || {});
75195
$btn.text(BCCImageOCRWithOpenAI.i18n.done);
76196
window.setTimeout(function () {
77197
$btn.prop('disabled', false).text(originalLabel);
@@ -89,22 +209,26 @@
89209
});
90210
}
91211

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) {
94218
return true;
95219
}
96220

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+
99224
if ($editBtn.length) {
100225
const $btn = createButton();
101226
$editBtn.after($btn);
102227
$btn.on('click', function (e) { e.preventDefault(); run($btn); });
103228
return true;
104229
}
105230

106-
// Fallback: above the description textarea.
107-
const $desc = $(SELECTORS.descTextarea);
231+
const $desc = $(EDIT_SCREEN.descTextarea);
108232
if ($desc.length) {
109233
const $btn = createButton().css({ marginLeft: 0, marginBottom: '6px', display: 'inline-block' });
110234
$desc.before($btn);
@@ -115,18 +239,64 @@
115239
return false;
116240
}
117241

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+
118269
$(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+
});
121299
}
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);
130300
});
131301

132302
})(jQuery);

0 commit comments

Comments
 (0)