Skip to content

Commit 9793a18

Browse files
committed
Simultaneously Displaying Multilingual Metadata on the Article Landing Page
1 parent 8e5353d commit 9793a18

File tree

6 files changed

+467
-26
lines changed

6 files changed

+467
-26
lines changed

pages/preprint/PreprintHandler.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use APP\facades\Repo;
2323
use APP\handler\Handler;
2424
use APP\observers\events\UsageEvent;
25+
use APP\publication\Publication;
2526
use APP\security\authorization\OpsServerMustPublishPolicy;
2627
use APP\template\TemplateManager;
2728
use Firebase\JWT\JWT;
@@ -297,6 +298,12 @@ public function view($args, $request)
297298
$templateMgr->addHeader('canonical', '<link rel="canonical" href="' . $url . '">');
298299
}
299300

301+
$templateMgr->assign('pubLocaleData', $this->getMultilingualMetadataOpts(
302+
$publication,
303+
$templateMgr->getTemplateVars('currentLocale'),
304+
$templateMgr->getTemplateVars('activeTheme')->getOption('showMultilingualMetadata') ?: [],
305+
));
306+
300307
if (!Hook::call('PreprintHandler::view', [&$request, &$preprint, $publication])) {
301308
$templateMgr->display('frontend/pages/preprint.tpl');
302309
event(new UsageEvent(Application::ASSOC_TYPE_SUBMISSION, $context, $preprint));
@@ -419,4 +426,26 @@ public function userCanViewGalley($request)
419426
}
420427
return false;
421428
}
429+
430+
/**
431+
* Multilingual publication metadata for template:
432+
* showMultilingualMetadataOpts - Show metadata in other languages: title (+ subtitle), keywords, abstract, etc.
433+
*/
434+
protected function getMultilingualMetadataOpts(Publication $publication, string $currentUILocale, array $showMultilingualMetadataOpts): array
435+
{
436+
$langNames = collect($publication->getLanguageNames())
437+
->sortKeys();
438+
$langs = $langNames->keys();
439+
return [
440+
'opts' => array_flip($showMultilingualMetadataOpts),
441+
'localeNames' => $langNames,
442+
'langAttrs' => $langNames->map(fn ($_, $l) => preg_replace(['/@.+$/', '/_/'], ['', '-'], $l))->toArray() /* remove @ and text after */,
443+
'localeOrder' => collect($publication->getLocalePrecedence())
444+
->intersect($langs) /* remove locales not in publication's languages */
445+
->concat($langs)
446+
->unique()
447+
->values()
448+
->toArray(),
449+
];
450+
}
422451
}

plugins/themes/default/DefaultThemePlugin.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,30 @@ public function init()
125125
'default' => 'none',
126126
]);
127127

128+
$this->addOption('showMultilingualMetadata', 'FieldOptions', [
129+
'label' => __('plugins.themes.default.option.metadata.label'),
130+
'description' => __('plugins.themes.default.option.metadata.description'),
131+
'options' => [
132+
[
133+
'value' => 'title',
134+
'label' => __('submission.title'),
135+
],
136+
[
137+
'value' => 'keywords',
138+
'label' => __('common.keywords'),
139+
],
140+
[
141+
'value' => 'abstract',
142+
'label' => __('common.abstract'),
143+
],
144+
[
145+
'value' => 'author',
146+
'label' => __('default.groups.name.author'),
147+
],
148+
],
149+
'default' => [],
150+
]);
151+
128152
// Load primary stylesheet
129153
$this->addStyle('stylesheet', 'styles/index.less');
130154

plugins/themes/default/js/main.js

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,177 @@
114114
});
115115

116116
})(jQuery);
117+
118+
/**
119+
* Create language buttons to show multilingual metadata
120+
* [data-pkp-switcher-data]: Publication data for the switchers to control
121+
* [data-pkp-switcher]: Switchers' containers
122+
*/
123+
(() => {
124+
function createSwitcher(switcherContainer, data, localeOrder, localeNames) {
125+
// Get all locales for the switcher from the data
126+
const locales = Object.keys(Object.assign({}, ...Object.values(data)));
127+
// The initially selected locale
128+
let selectedLocale = null;
129+
// Create and sort to alphabetical order
130+
const buttons = localeOrder
131+
.map((locale) => {
132+
if (locales.indexOf(locale) === -1) {
133+
return null;
134+
}
135+
if (!selectedLocale) {
136+
selectedLocale = locale;
137+
}
138+
139+
const isSelectedLocale = locale === selectedLocale;
140+
const button = document.createElement('button');
141+
142+
button.type = 'button';
143+
button.classList.add('pkpBadge', 'pkpBadge--button');
144+
button.value = locale;
145+
button.tabIndex = '-1';
146+
button.role = 'option';
147+
button.ariaHidden = `${!isSelectedLocale}`;
148+
button.textContent = localeNames[locale];
149+
if (isSelectedLocale) {
150+
button.ariaPressed = 'false';
151+
button.ariaCurrent = 'true';
152+
button.tabIndex = '0';
153+
}
154+
return button;
155+
})
156+
.filter((btn) => btn)
157+
.sort((a, b) => a.value.localeCompare(b.value));
158+
159+
// If only one button, set it disabled
160+
if (buttons.length === 1) {
161+
buttons[0].disabled = true;
162+
}
163+
164+
buttons.forEach((btn, i) => {
165+
switcherContainer.appendChild(btn);
166+
});
167+
168+
return buttons;
169+
}
170+
171+
/**
172+
* Sync data in elements to match the selected locale
173+
*/
174+
function syncDataElContents(locale, propsData, langAttrs) {
175+
for (prop in propsData.data) {
176+
propsData.dataEls[prop].lang = langAttrs[locale];
177+
propsData.dataEls[prop].innerHTML = propsData.data[prop][locale] ?? '';
178+
}
179+
}
180+
181+
/**
182+
* Toggle visibility of the buttons
183+
* setValue == true => aria-hidden == true, aria-expanded == false
184+
*/
185+
function setVisibility(switcherContainer, buttons, currentSelected, setValue) {
186+
// Toggle switcher container's listbox/none-role
187+
// Listbox when buttons visible and none when hidden
188+
switcherContainer.role = setValue ? 'none' : 'listbox';
189+
currentSelected.btn.ariaPressed = `${!setValue}`;
190+
buttons.forEach((btn) => {
191+
if (btn !== currentSelected.btn) {
192+
btn.ariaHidden = `${setValue}`;
193+
}
194+
});
195+
switcherContainer.ariaExpanded = `${!setValue}`;
196+
}
197+
198+
function setSwitcher(propsData, switcherContainer, localeOrder, localeNames, langAttrs) {
199+
// Create buttons and append them to the switcher container
200+
const buttons = createSwitcher(switcherContainer, propsData.data, localeOrder, localeNames);
201+
const currentSelected = {btn: switcherContainer.querySelector('[tabindex="0"]')};
202+
const focused = {btn: currentSelected.btn};
203+
204+
// Sync contents in data elements to match the selected locale (currentSelected.btn.value)
205+
syncDataElContents(currentSelected.btn.value, propsData, langAttrs);
206+
207+
// Do not add listeners if just one button, it is disabled
208+
if (buttons.length < 2) {
209+
return;
210+
}
211+
212+
// New button switches language and syncs data contents. Same button hides buttons.
213+
switcherContainer.addEventListener('click', (evt) => {
214+
const newSelectedBtn = evt.target;
215+
if (newSelectedBtn.type === 'button') {
216+
if (newSelectedBtn !== currentSelected.btn) {
217+
syncDataElContents(newSelectedBtn.value, propsData, langAttrs);
218+
// Aria
219+
currentSelected.btn.ariaCurrent = null;
220+
newSelectedBtn.ariaCurrent = 'true';
221+
currentSelected.btn.ariaPressed = null;
222+
newSelectedBtn.ariaPressed = 'true';
223+
// Tab index
224+
currentSelected.btn.tabIndex = '-1';
225+
newSelectedBtn.tabIndex = '0';
226+
// Update current and focused button
227+
currentSelected.btn = focused.btn = newSelectedBtn;
228+
focused.btn.focus();
229+
} else {
230+
setVisibility(switcherContainer, buttons, currentSelected, switcherContainer.ariaExpanded === 'true');
231+
}
232+
}
233+
});
234+
235+
// Hide buttons when focus out
236+
switcherContainer.addEventListener('focusout', (evt) => {
237+
// For safari losing button focus
238+
if (evt.target.parentElement === switcherContainer && switcherContainer.ariaExpanded === 'true') {
239+
focused.btn.focus();
240+
}
241+
if (!evt.relatedTarget || evt.relatedTarget && evt.relatedTarget.parentElement !== switcherContainer) {
242+
setVisibility(switcherContainer, buttons, currentSelected, 'true');
243+
}
244+
});
245+
246+
// Arrow keys left and right cycles button focus when buttons visible. Set focused button.
247+
switcherContainer.addEventListener("keydown", (evt) => {
248+
if (switcherContainer.ariaExpanded === 'true' && evt.target.type === 'button' && (evt.key === "ArrowRight" || evt.key === "ArrowLeft")) {
249+
focused.btn = (evt.key === "ArrowRight")
250+
? (focused.btn.nextElementSibling ?? buttons[0])
251+
: (focused.btn.previousElementSibling ?? buttons[buttons.length - 1]);
252+
focused.btn.focus();
253+
}
254+
});
255+
}
256+
257+
/**
258+
* Set all multilingual data and elements for the switchers
259+
*/
260+
function setSwitchersData(dataEls, pubLocaleData) {
261+
const propsData = {};
262+
dataEls.forEach((dataEl) => {
263+
const propName = dataEl.getAttribute('data-pkp-switcher-data');
264+
const switcherName = pubLocaleData[propName].switcher;
265+
if (!propsData[switcherName]) {
266+
propsData[switcherName] = {data: [], dataEls: []};
267+
}
268+
propsData[switcherName].data[propName] = pubLocaleData[propName].data;
269+
propsData[switcherName].dataEls[propName] = dataEl;
270+
});
271+
return propsData;
272+
}
273+
274+
(() => {
275+
const switcherContainers = document.querySelectorAll('[data-pkp-switcher]');
276+
277+
if (!switcherContainers.length) return;
278+
279+
const pubLocaleData = JSON.parse(pubLocaleDataJson);
280+
const switchersDataEls = document.querySelectorAll('[data-pkp-switcher-data]');
281+
const switchersData = setSwitchersData(switchersDataEls, pubLocaleData);
282+
// Create and set switchers, and sync data on the page
283+
switcherContainers.forEach((switcherContainer) => {
284+
const switcherName = switcherContainer.getAttribute('data-pkp-switcher');
285+
if (switchersData[switcherName]) {
286+
setSwitcher(switchersData[switcherName], switcherContainer, pubLocaleData.localeOrder, pubLocaleData.localeNames, pubLocaleData.langAttrs);
287+
}
288+
});
289+
})();
290+
})();

plugins/themes/default/locale/en/locale.po

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,22 @@ msgid "plugins.themes.default.nextSlide"
9797
msgstr "Next slide"
9898

9999
msgid "plugins.themes.default.prevSlide"
100-
msgstr "Previous slide"
100+
msgstr "Previous slide"
101+
102+
msgid "plugins.themes.default.option.metadata.label"
103+
msgstr "Show preprint metadata on the preprint landing page"
104+
105+
msgid "plugins.themes.default.option.metadata.description"
106+
msgstr "Select the preprint metadata to show in other languages."
107+
108+
msgid "plugins.themes.default.languageSwitcher.ariaDescription.titles"
109+
msgstr "The preprint title and subtitle languages:"
110+
111+
msgid "plugins.themes.default.languageSwitcher.ariaDescription.author"
112+
msgstr "The author's affiliation languages:"
113+
114+
msgid "plugins.themes.default.languageSwitcher.ariaDescription.keywords"
115+
msgstr "The keywords languages:"
116+
117+
msgid "plugins.themes.default.languageSwitcher.ariaDescription.abstract"
118+
msgstr "The abstract languages:"

plugins/themes/default/styles/objects/preprint_details.less

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@
6060
font-size: @font-bump;
6161
font-weight: @bold;
6262
}
63+
64+
&.keywords .label,
65+
&.abstract .label {
66+
display: inline-flex;
67+
font-size: @font-base;
68+
}
6369
}
6470

6571
.sub_item {
@@ -268,6 +274,71 @@
268274
}
269275
}
270276

277+
/**
278+
* Language switcher
279+
*/
280+
281+
.pkpBadge {
282+
padding: 0.25em 1em;
283+
font-size: @font-tiny;
284+
font-weight: @normal;
285+
line-height: 1.5em;
286+
border: 1px solid @bg-border-color-light;
287+
border-radius: 1.2em;
288+
color: @text;
289+
}
290+
291+
.pkpBadge--button {
292+
background: inherit;
293+
text-decoration: none;
294+
cursor: pointer;
295+
296+
&:hover {
297+
border-color: @text;
298+
outline: 0;
299+
}
300+
&:disabled,
301+
&:disabled:hover {
302+
color: #fff;
303+
background: @bg-dark;
304+
border-color: @bg-dark;
305+
cursor: not-allowed;
306+
}
307+
}
308+
309+
[data-pkp-switcher] [tabindex="0"] {
310+
font-weight: @bold;
311+
}
312+
313+
[data-pkp-switcher],
314+
[data-pkp-switcher] * {
315+
display: inline-flex;
316+
}
317+
318+
[data-pkp-switcher] [aria-hidden="true"] {
319+
display: none;
320+
}
321+
322+
[data-pkp-switcher] [aria-hidden="false"] {
323+
animation: fadeIn 0.7s ease-in-out;
324+
325+
@keyframes fadeIn {
326+
0% {
327+
display: none;
328+
opacity: 0;
329+
}
330+
331+
1% {
332+
display: inline-flex;
333+
opacity: 0;
334+
}
335+
336+
100% {
337+
opacity: 1;
338+
}
339+
}
340+
}
341+
271342
@media(min-width: @screen-phone) {
272343

273344
.entry_details {
@@ -302,8 +373,7 @@
302373
font-weight: @bold;
303374
}
304375

305-
&.doi .label,
306-
&.keywords .label {
376+
&.doi .label {
307377
display: inline;
308378
font-size: @font-base;
309379
}

0 commit comments

Comments
 (0)