Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pkp/pkp-lib#7272 Simultaneously Displaying Multilingual Metadata on the Article Landing Page #4050

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading