Skip to content

Commit

Permalink
Simultaneously Displaying Multilingual Metadata on the Article Landin…
Browse files Browse the repository at this point in the history
…g Page
  • Loading branch information
jyhein committed Jan 20, 2025
1 parent fa7f1f9 commit cbabad0
Show file tree
Hide file tree
Showing 6 changed files with 467 additions and 26 deletions.
29 changes: 29 additions & 0 deletions pages/article/ArticleHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use APP\observers\events\UsageEvent;
use APP\payment\ojs\OJSCompletedPaymentDAO;
use APP\payment\ojs\OJSPaymentManager;
use APP\publication\Publication;
use APP\security\authorization\OjsJournalMustPublishPolicy;
use APP\submission\Submission;
use APP\template\TemplateManager;
Expand Down Expand Up @@ -360,6 +361,12 @@ public function view($args, $request)
$templateMgr->assign('purchaseArticleEnabled', true);
}

$templateMgr->assign('pubLocaleData', $this->getMultilingualMetadataOpts(
$publication,
$templateMgr->getTemplateVars('currentLocale'),
$templateMgr->getTemplateVars('activeTheme')->getOption('showMultilingualMetadata') ?: [],
));

if (!Hook::call('ArticleHandler::view', [&$request, &$issue, &$article, $publication])) {
$templateMgr->display('frontend/pages/article.tpl');
event(new UsageEvent(Application::ASSOC_TYPE_SUBMISSION, $context, $article, null, null, $this->issue));
Expand Down Expand Up @@ -617,4 +624,26 @@ public function userCanViewGalley($request, $articleId, $galleyId = null)
}
return true;
}

/**
* Multilingual publication metadata for template:
* showMultilingualMetadataOpts - Show metadata in other languages: title (+ subtitle), keywords, abstract, etc.
*/
protected function getMultilingualMetadataOpts(Publication $publication, string $currentUILocale, array $showMultilingualMetadataOpts): array
{
$langNames = collect($publication->getLanguageNames())
->sortKeys();
$langs = $langNames->keys();
return [
'opts' => array_flip($showMultilingualMetadataOpts),
'localeNames' => $langNames,
'langAttrs' => $langNames->map(fn ($_, $l) => preg_replace(['/@.+$/', '/_/'], ['', '-'], $l))->toArray() /* remove @ and text after */,
'localeOrder' => collect($publication->getLocalePrecedence())
->intersect($langs) /* remove locales not in publication's languages */
->concat($langs)
->unique()
->values()
->toArray(),
];
}
}
24 changes: 24 additions & 0 deletions plugins/themes/default/DefaultThemePlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,30 @@ public function init()
'default' => 'none',
]);

$this->addOption('showMultilingualMetadata', 'FieldOptions', [
'label' => __('plugins.themes.default.option.metadata.label'),
'description' => __('plugins.themes.default.option.metadata.description'),
'options' => [
[
'value' => 'title',
'label' => __('submission.title'),
],
[
'value' => 'abstract',
'label' => __('common.abstract'),
],
[
'value' => 'keywords',
'label' => __('common.keywords'),
],
[
'value' => 'author',
'label' => __('default.groups.name.author'),
],
],
'default' => [],
]);


// Load primary stylesheet
$this->addStyle('stylesheet', 'styles/index.less');
Expand Down
174 changes: 174 additions & 0 deletions plugins/themes/default/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,177 @@
});

})(jQuery);

/**
* Create language buttons to show multilingual metadata
* [data-pkp-switcher-data]: Publication data for the switchers to control
* [data-pkp-switcher]: Switchers' containers
*/
(() => {
function createSwitcher(switcherContainer, data, localeOrder, localeNames) {
// Get all locales for the switcher from the data
const locales = Object.keys(Object.assign({}, ...Object.values(data)));
// The initially selected locale
let selectedLocale = null;
// Create and sort to alphabetical order
const buttons = localeOrder
.map((locale) => {
if (locales.indexOf(locale) === -1) {
return null;
}
if (!selectedLocale) {
selectedLocale = locale;
}

const isSelectedLocale = locale === selectedLocale;
const button = document.createElement('button');

button.type = 'button';
button.classList.add('pkpBadge', 'pkpBadge--button');
button.value = locale;
button.tabIndex = '-1';
button.role = 'option';
button.ariaHidden = `${!isSelectedLocale}`;
button.textContent = localeNames[locale];
if (isSelectedLocale) {
button.ariaPressed = 'false';
button.ariaCurrent = 'true';
button.tabIndex = '0';
}
return button;
})
.filter((btn) => btn)
.sort((a, b) => a.value.localeCompare(b.value));

// If only one button, set it disabled
if (buttons.length === 1) {
buttons[0].disabled = true;
}

buttons.forEach((btn, i) => {
switcherContainer.appendChild(btn);
});

return buttons;
}

/**
* Sync data in elements to match the selected locale
*/
function syncDataElContents(locale, propsData, langAttrs) {
for (prop in propsData.data) {
propsData.dataEls[prop].lang = langAttrs[locale];
propsData.dataEls[prop].innerHTML = propsData.data[prop][locale] ?? '';
}
}

/**
* Toggle visibility of the buttons
* setValue == true => aria-hidden == true, aria-expanded == false
*/
function setVisibility(switcherContainer, buttons, currentSelected, setValue) {
// Toggle switcher container's listbox/none-role
// Listbox when buttons visible and none when hidden
switcherContainer.role = setValue ? 'none' : 'listbox';
currentSelected.btn.ariaPressed = `${!setValue}`;
buttons.forEach((btn) => {
if (btn !== currentSelected.btn) {
btn.ariaHidden = `${setValue}`;
}
});
switcherContainer.ariaExpanded = `${!setValue}`;
}

function setSwitcher(propsData, switcherContainer, localeOrder, localeNames, langAttrs) {
// Create buttons and append them to the switcher container
const buttons = createSwitcher(switcherContainer, propsData.data, localeOrder, localeNames);
const currentSelected = {btn: switcherContainer.querySelector('[tabindex="0"]')};
const focused = {btn: currentSelected.btn};

// Sync contents in data elements to match the selected locale (currentSelected.btn.value)
syncDataElContents(currentSelected.btn.value, propsData, langAttrs);

// Do not add listeners if just one button, it is disabled
if (buttons.length < 2) {
return;
}

// New button switches language and syncs data contents. Same button hides buttons.
switcherContainer.addEventListener('click', (evt) => {
const newSelectedBtn = evt.target;
if (newSelectedBtn.type === 'button') {
if (newSelectedBtn !== currentSelected.btn) {
syncDataElContents(newSelectedBtn.value, propsData, langAttrs);
// Aria
currentSelected.btn.ariaCurrent = null;
newSelectedBtn.ariaCurrent = 'true';
currentSelected.btn.ariaPressed = null;
newSelectedBtn.ariaPressed = 'true';
// Tab index
currentSelected.btn.tabIndex = '-1';
newSelectedBtn.tabIndex = '0';
// Update current and focused button
currentSelected.btn = focused.btn = newSelectedBtn;
focused.btn.focus();
} else {
setVisibility(switcherContainer, buttons, currentSelected, switcherContainer.ariaExpanded === 'true');
}
}
});

// Hide buttons when focus out
switcherContainer.addEventListener('focusout', (evt) => {
// For safari losing button focus
if (evt.target.parentElement === switcherContainer && switcherContainer.ariaExpanded === 'true') {
focused.btn.focus();
}
if (!evt.relatedTarget || evt.relatedTarget && evt.relatedTarget.parentElement !== switcherContainer) {
setVisibility(switcherContainer, buttons, currentSelected, 'true');
}
});

// Arrow keys left and right cycles button focus when buttons visible. Set focused button.
switcherContainer.addEventListener("keydown", (evt) => {
if (switcherContainer.ariaExpanded === 'true' && evt.target.type === 'button' && (evt.key === "ArrowRight" || evt.key === "ArrowLeft")) {
focused.btn = (evt.key === "ArrowRight")
? (focused.btn.nextElementSibling ?? buttons[0])
: (focused.btn.previousElementSibling ?? buttons[buttons.length - 1]);
focused.btn.focus();
}
});
}

/**
* Set all multilingual data and elements for the switchers
*/
function setSwitchersData(dataEls, pubLocaleData) {
const propsData = {};
dataEls.forEach((dataEl) => {
const propName = dataEl.getAttribute('data-pkp-switcher-data');
const switcherName = pubLocaleData[propName].switcher;
if (!propsData[switcherName]) {
propsData[switcherName] = {data: [], dataEls: []};
}
propsData[switcherName].data[propName] = pubLocaleData[propName].data;
propsData[switcherName].dataEls[propName] = dataEl;
});
return propsData;
}

(() => {
const switcherContainers = document.querySelectorAll('[data-pkp-switcher]');

if (!switcherContainers.length) return;

const pubLocaleData = JSON.parse(pubLocaleDataJson);
const switchersDataEls = document.querySelectorAll('[data-pkp-switcher-data]');
const switchersData = setSwitchersData(switchersDataEls, pubLocaleData);
// Create and set switchers, and sync data on the page
switcherContainers.forEach((switcherContainer) => {
const switcherName = switcherContainer.getAttribute('data-pkp-switcher');
if (switchersData[switcherName]) {
setSwitcher(switchersData[switcherName], switcherContainer, pubLocaleData.localeOrder, pubLocaleData.localeNames, pubLocaleData.langAttrs);
}
});
})();
})();
18 changes: 18 additions & 0 deletions plugins/themes/default/locale/en/locale.po
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,21 @@ msgstr "Next slide"

msgid "plugins.themes.default.prevSlide"
msgstr "Previous slide"

msgid "plugins.themes.default.option.metadata.label"
msgstr "Show article metadata on the article landing page"

msgid "plugins.themes.default.option.metadata.description"
msgstr "Select the article metadata to show in other languages."

msgid "plugins.themes.default.languageSwitcher.ariaDescription.titles"
msgstr "The article title and subtitle languages:"

msgid "plugins.themes.default.languageSwitcher.ariaDescription.author"
msgstr "The author's affiliation languages:"

msgid "plugins.themes.default.languageSwitcher.ariaDescription.keywords"
msgstr "The keywords languages:"

msgid "plugins.themes.default.languageSwitcher.ariaDescription.abstract"
msgstr "The abstract languages:"
77 changes: 73 additions & 4 deletions plugins/themes/default/styles/objects/article_details.less
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,16 @@
font-weight: @bold;
}

&.doi .label,
&.keywords .label {
&.doi .label {
display: inline;
font-size: @font-base;
}

&.keywords .label,
&.abstract .label {
display: inline-flex;
font-size: @font-base;
}
}

.sub_item {
Expand Down Expand Up @@ -281,6 +286,71 @@
}
}

/**
* Language switcher
*/

.pkpBadge {
padding: 0.25em 1em;
font-size: @font-tiny;
font-weight: @normal;
line-height: 1.5em;
border: 1px solid @bg-border-color-light;
border-radius: 1.2em;
color: @text;
}

.pkpBadge--button {
background: inherit;
text-decoration: none;
cursor: pointer;

&:hover {
border-color: @text;
outline: 0;
}
&:disabled,
&:disabled:hover {
color: #fff;
background: @bg-dark;
border-color: @bg-dark;
cursor: not-allowed;
}
}

[data-pkp-switcher] [tabindex="0"] {
font-weight: @bold;
}

[data-pkp-switcher],
[data-pkp-switcher] * {
display: inline-flex;
}

[data-pkp-switcher] [aria-hidden="true"] {
display: none;
}

[data-pkp-switcher] [aria-hidden="false"] {
animation: fadeIn 0.7s ease-in-out;

@keyframes fadeIn {
0% {
display: none;
opacity: 0;
}

1% {
display: inline-flex;
opacity: 0;
}

100% {
opacity: 1;
}
}
}

@media(min-width: @screen-phone) {

.entry_details {
Expand Down Expand Up @@ -315,8 +385,7 @@
font-weight: @bold;
}

&.doi .label,
&.keywords .label {
&.doi .label {
display: inline;
font-size: @font-base;
}
Expand Down
Loading

0 comments on commit cbabad0

Please sign in to comment.