-
Reiter, Christoph authored
This is inspired by how lion web components handle this, and much cleaner.
Reiter, Christoph authoredThis is inspired by how lion web components handle this, and much cleaner.
resource-select.js 7.76 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');
}
}
buildUrl(select, url) {
return url;
}
formatResource(select, resource) {
return resource.name ?? resource['@id'];
}
_getUrl() {
if (this.entryPointUrl === null) {
return null;
}
let url = this.entryPointUrl;
if (this.resourcePath !== null) {
url = new URL(this.resourcePath, this.entryPointUrl).href;
}
url = this.buildUrl(this, url);
return url;
}
_getText(resource) {
return this.formatResource(this, resource);
}
_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>
`;
}
}