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> `; } }