-
Reiter, Christoph authored
The goal of this new component is to be a generic selector for API resources, unlike the organization/person select, which hardcode various things for their used resource. Instead this provides some events which can be used to customize the creation of the URL for fetching as well as for formatting the list of results. Atm this only handles the organization and no search-as-you-type like the person select, but maybe this can be extended in the future.
Reiter, Christoph authoredThe goal of this new component is to be a generic selector for API resources, unlike the organization/person select, which hardcode various things for their used resource. Instead this provides some events which can be used to customize the creation of the URL for fetching as well as for formatting the list of results. Atm this only handles the organization and no search-as-you-type like the person select, but maybe this can be extended in the future.
resource-select.js 7.97 KiB
import $ from 'jquery';
import select2 from 'select2';
import select2CSSPath from 'select2/dist/css/select2.min.css';
import {createInstance} from './i18n.js';
import {css, html} from 'lit';
import * as commonUtils from '@dbp-toolkit/common/utils';
import * as commonStyles from '@dbp-toolkit/common/styles';
import select2LangDe from '@dbp-toolkit/resource-select/src/i18n/de/select2';
import select2LangEn from '@dbp-toolkit/resource-select/src/i18n/en/select2';
import {AdapterLitElement} from '@dbp-toolkit/provider/src/adapter-lit-element';
import * as hydra from './hydra.js';
export class ResourceSelect extends AdapterLitElement {
constructor() {
super();
this._i18n = createInstance();
this._resources = [];
this._url = null;
// For some reason using the same ID on the whole page twice breaks select2 (regardless if they are in different custom elements)
this._selectId = 'select-resource-' + commonUtils.makeId(24);
this.auth = {};
this.lang = this._i18n.language;
this.entryPointUrl = null;
this.resourcePath = null;
this.value = null;
this.valueObject = null;
this._onDocumentClicked = this._onDocumentClicked.bind(this);
select2(window, $);
}
static get properties() {
return {
...super.properties,
lang: {type: String},
auth: {type: Object},
entryPointUrl: {type: String, attribute: 'entry-point-url'},
resourcePath: {type: String, attribute: 'resource-path'},
value: {type: String, reflect: true},
};
}
_getSelect2() {
return this._$('#' + this._selectId);
}
_$(selector) {
return $(this.renderRoot.querySelector(selector));
}
_IsSelect2Initialized(elm) {
return elm !== null && elm.hasClass('select2-hidden-accessible');
}
connectedCallback() {
super.connectedCallback();
document.addEventListener('click', this._onDocumentClicked);
this._updateAll();
}
disconnectedCallback() {
document.removeEventListener('click', this._onDocumentClicked);
super.disconnectedCallback();
}
_onDocumentClicked(ev) {
// Close the popup when clicking outside of select2
if (!ev.composedPath().includes(this)) {
const $select = this._getSelect2();
if ($select.length && this._IsSelect2Initialized($select)) {
$select.select2('close');
}
}
}
_clearSelect2() {
const $select = this._getSelect2();
console.assert($select.length, 'select2 missing');
// we need to destroy Select2 and remove the event listeners before we can initialize it again
if (this._IsSelect2Initialized($select)) {
$select.off('select2:select');
$select.empty().trigger('change');
$select.select2('destroy');
}
}
_getUrl() {
if (this.entryPointUrl === null) {
return null;
}
let url = this.entryPointUrl;
if (this.resourcePath !== null) {
url = new URL(this.resourcePath, this.entryPointUrl).href;
}
let detail = {
url: url,
};
const event = new CustomEvent('build-url', {
detail: detail,
});
this.dispatchEvent(event);
return detail.url;
}
_getText(resource) {
let detail = {
text: resource.name ?? null,
object: resource,
};
const event = new CustomEvent('format-resource', {
detail: detail,
});
this.dispatchEvent(event);
return detail.text;
}
_setValue(value) {
let changed = false;
changed = this.value !== value;
this.value = value;
let found = null;
for (let res of this._resources) {
if (res['@id'] === this.value) {
found = res;
break;
}
}
changed = changed || this.valueObject !== found;
this.valueObject = found;
if (!changed) {
return;
}
const event = new CustomEvent('change', {
bubbles: true,
composed: true,
detail: {
value: this.value,
object: this.valueObject,
},
});
this.dispatchEvent(event);
}
async _updateAll() {
this._setValue(this.value);
if (!this.auth.token) {
await this._setSelect2Loading();
return;
}
await this._updateResources();
await this._updateSelect2();
}
async _setSelect2Loading() {
await this.updateComplete;
const i18n = this._i18n;
const $select = this._getSelect2();
console.assert($select.length, 'select2 missing');
// Show an empty select until we load the resources
this._clearSelect2();
$select.select2({
width: '100%',
language: this.lang === 'de' ? select2LangDe() : select2LangEn(),
placeholder: i18n.t('select.loading'),
data: [],
disabled: true,
});
}
async _updateResources() {
let url = this._getUrl();
if (url === null || url === this._url) {
return;
}
this._resources = await hydra.getCollection(url, this.auth.token);
this._url = url;
this._setValue(this.value);
}
async _updateSelect2() {
await this.updateComplete;
const i18n = this._i18n;
const $select = this._getSelect2();
console.assert($select.length, 'select2 missing');
const data = this._resources.map((item) => {
return {id: item['@id'], text: this._getText(item)};
});
data.sort((a, b) => {
return a.text < b.text ? -1 : a.text > b.text ? 1 : 0;
});
this._clearSelect2();
$select
.select2({
width: '100%',
language: this.lang === 'de' ? select2LangDe() : select2LangEn(),
placeholder: i18n.t('select.placeholder'),
dropdownParent: this._$('#select-resource-dropdown'),
data: data,
disabled: false,
})
.on('select2:select', () => {
let id = $select.select2('data')[0].id;
this._setValue(id);
});
// If none is selected, default to the first one
if (this.value === null && data.length) {
this._setValue(data[0].id);
}
// Apply the selection
$select.val(this.value).trigger('change');
}
update(changedProperties) {
if (changedProperties.has('lang')) {
this._i18n.changeLanguage(this.lang);
}
if (
changedProperties.has('lang') ||
changedProperties.has('value') ||
changedProperties.has('resourcePath') ||
changedProperties.has('entryPointUrl') ||
changedProperties.has('auth')
) {
this._updateAll();
}
super.update(changedProperties);
}
static get styles() {
return [
commonStyles.getThemeCSS(),
commonStyles.getGeneralCSS(),
commonStyles.getNotificationCSS(),
commonStyles.getSelect2CSS(),
// language=css
css``,
];
}
render() {
const select2CSS = commonUtils.getAssetURL(select2CSSPath);
return html`
<link rel="stylesheet" href="${select2CSS}" />
<div class="select">
<div class="select2-control control">
<select
id="${this._selectId}"
name="select-resources"
class="select"
style="visibility: hidden;"></select>
</div>
<div id="select-resource-dropdown"></div>
</div>
`;
}
}