Skip to content
Snippets Groups Projects
check-in-place-select.js 16.8 KiB
Newer Older
import {findObjectInApiResults} from './utils.js';
import select2LangDe from './i18n/de/select2';
import select2LangEn from './i18n/en/select2';
import JSONLD from '@dbp-toolkit/common/jsonld';
import {css, html} from 'lit';
import {ScopedElementsMixin} from '@open-wc/scoped-elements';
import {createInstance} from './i18n.js';
import {Icon} from '@dbp-toolkit/common';
import * as commonUtils from '@dbp-toolkit/common/utils';
import * as commonStyles from '@dbp-toolkit/common/styles';
import select2CSSPath from 'select2/dist/css/select2.min.css';
Reiter, Christoph's avatar
Reiter, Christoph committed
import * as errorUtils from '@dbp-toolkit/common/error';
import {AdapterLitElement} from '@dbp-toolkit/provider/src/adapter-lit-element';
const checkInPlaceContext = {
Reiter, Christoph's avatar
Reiter, Christoph committed
    '@id': '@id',
    name: 'http://schema.org/name',
    maximumPhysicalAttendeeCapacity: 'http://schema.org/maximumPhysicalAttendeeCapacity',
export class CheckInPlaceSelect extends ScopedElementsMixin(AdapterLitElement) {
    constructor() {
        super();
Bekerle, Patrizio's avatar
Bekerle, Patrizio committed
        Object.assign(CheckInPlaceSelect.prototype, errorUtils.errorMixin);
        this.entryPointUrl = '';
        this.jsonld = null;
        this.$select = null;
        this.active = false;
        // For some reason using the same ID on the whole page twice breaks select2 (regardless if they are in different custom elements)
        this.selectId = 'check-in-place-select-' + commonUtils.makeId(24);
        this.value = '';
        this.object = null;
        this.ignoreValueUpdate = false;
        this.isSearching = false;
        this.lastResult = {};
        this.showReloadButton = false;
        this.reloadButtonTitle = '';
        this.showCapacity = false;

        this._onDocumentClicked = this._onDocumentClicked.bind(this);
    }

    static get scopedElements() {
        return {
Reiter, Christoph's avatar
Reiter, Christoph committed
            'dbp-icon': Icon,
Reiter, Christoph's avatar
Reiter, Christoph committed
        if (typeof selector === 'string')
            return this._jquery(this.shadowRoot.querySelector(selector));
Reiter, Christoph's avatar
Reiter, Christoph committed
        else return this._jquery(selector);
    }

    static get properties() {
        return {
            ...super.properties,
Reiter, Christoph's avatar
Reiter, Christoph committed
            lang: {type: String},
            active: {type: Boolean, attribute: false},
            entryPointUrl: {type: String, attribute: 'entry-point-url'},
            value: {type: String},
            object: {type: Object, attribute: false},
            showReloadButton: {type: Boolean, attribute: 'show-reload-button'},
            reloadButtonTitle: {type: String, attribute: 'reload-button-title'},
            showCapacity: {type: Boolean, attribute: 'show-capacity'},
            auth: {type: Object},
    }

    clear() {
        this.object = null;
Reiter, Christoph's avatar
Reiter, Christoph committed
        this.$(this).attr('data-object', '');
        this.$(this).attr('value', '');
        this.$(this).data('object', null);
        this.$select.val(null).trigger('change').trigger('select2:unselect');
    }

    async connectedCallback() {
        this._jquery = (await import('jquery')).default;
        let select2 = (await import('select2')).default;
        select2(window, this._jquery);
        await super.connectedCallback();
        await this.updateComplete;
        const that = this;
        this.$select = this.$('#' + that.selectId);
        // Close the popup when clicking outside of select2
        document.addEventListener('click', this._onDocumentClicked);

        // try an init when user-interface is loaded
        this.initJSONLD();
    disconnectedCallback() {
        document.removeEventListener('click', this._onDocumentClicked);
        super.disconnectedCallback();
    }

    _onDocumentClicked(event) {
Reiter, Christoph's avatar
Reiter, Christoph committed
        if (event.composedPath().includes(this)) return;
        const $select = this.$('#' + this.selectId);
Reiter, Christoph's avatar
Reiter, Christoph committed
        console.assert($select.length, 'select2 missing');
        if (this.select2IsInitialized($select)) {
            $select.select2('close');
        }
    }

    initJSONLD(ignorePreset = false) {
        const that = this;

Reiter, Christoph's avatar
Reiter, Christoph committed
        JSONLD.getInstance(this.entryPointUrl).then(
            function (jsonld) {
                that.jsonld = jsonld;
                that.active = true;
Reiter, Christoph's avatar
Reiter, Christoph committed
                // we need to poll because maybe the user interface isn't loaded yet
                // Note: we need to call initSelect2() in a different function so we can access "this" inside initSelect2()
                commonUtils.pollFunc(
                    () => {
                        return that.initSelect2(ignorePreset);
                    },
                    10000,
                    100
                );
            },
            {},
            this.lang
        );
    }

    /**
     * Initializes the Select2 selector
     *
     * @param ignorePreset
     */
    initSelect2(ignorePreset = false) {
        const that = this;

        if (this.jsonld === null) {
            return false;
        }

        // find the correct api url for a checkInPlace
        const apiUrl = this.jsonld.getApiUrlForEntityName("CheckinPlace");

        if (this.$select === null) {
            return false;
        }

        // we need to destroy Select2 and remove the event listeners before we can initialize it again
        if (this.$select && this.$select.hasClass('select2-hidden-accessible')) {
            this.$select.select2('destroy');
            this.$select.off('select2:select');
            this.$select.off('select2:closing');
        }

Reiter, Christoph's avatar
Reiter, Christoph committed
        this.$select
            .select2({
                width: '100%',
                language: this.lang === 'de' ? select2LangDe() : select2LangEn(),
                minimumInputLength: 2,
                placeholder: this._i18n.t('check-in-place-select.placeholder'),
                dropdownParent: this.$('#check-in-place-select-dropdown'),
                ajax: {
                    delay: 500,
                    url: apiUrl,
                    contentType: 'application/ld+json',
                    beforeSend: function (jqXHR) {
                        jqXHR.setRequestHeader('Authorization', 'Bearer ' + that.auth.token);
                        that.isSearching = true;
                    },
                    data: function (params) {
                        return {
                            search: params.term.trim(),
                        };
                    },
                    processResults: function (data) {
                        that.$('#check-in-place-select-dropdown').addClass('select2-bug');

                        that.lastResult = data;
                        let transformed = that.jsonld.transformMembers(data, checkInPlaceContext);
                        const results = [];
                        transformed.forEach((place) => {
                            results.push({
                                id: place['@id'],
                                maximumPhysicalAttendeeCapacity:
                                    place['maximumPhysicalAttendeeCapacity'],
                                text: that.generateOptionText(place),
                            });
                        });

                        return {
                            results: results,
                        };
                    },
                    error: (jqXHR, textStatus, errorThrown) => {
                        this.handleXhrError(jqXHR, textStatus, errorThrown);
                    },
                    complete: (jqXHR, textStatus) => {
                        that.isSearching = false;
                    },
Reiter, Christoph's avatar
Reiter, Christoph committed
            })
            .on('select2:open', function (e) {
                const selectId = e.target.id;

                that.$(
                    ".select2-search__field[aria-controls='select2-" + selectId + "-results']"
                ).each(function (key, value) {
                    value.focus();
                });
            })
            .on('select2:select', function (e) {
                that.$('#check-in-place-select-dropdown').removeClass('select2-bug');
                // set custom element attributes
                const identifier = e.params.data.id;
                const maxCapacity = e.params.data.maximumPhysicalAttendeeCapacity;
                that.object = findObjectInApiResults(identifier, that.lastResult);

                const room = that.object.identifier;

                $this.attr('data-object', JSON.stringify(that.object));
                $this.data('object', that.object);
                const roomName = that.object.name;

                if ($this.attr('value') !== identifier) {
                    that.ignoreValueUpdate = true;
                    $this.attr('value', identifier);

                    // fire a change event
                    that.dispatchEvent(
                        new CustomEvent('change', {
                            detail: {
                                value: identifier,
                                capacity: maxCapacity,
                                room: room,
                                name: roomName,
                            },
                            bubbles: true,
                        })
                    );
                }
            })
            .on('select2:closing', (e) => {
                if (that.isSearching) {
                    e.preventDefault();

        // TODO: doesn't work here
        // this.$('.select2-selection__arrow').click(() => {
        //     console.log("click");
        // });

        // preset a place
        if (!ignorePreset && this.value !== '') {
            const apiUrl = this.entryPointUrl + this.value;

            fetch(apiUrl, {
                headers: {
                    'Content-Type': 'application/ld+json',
Reiter, Christoph's avatar
Reiter, Christoph committed
                    Authorization: 'Bearer ' + this.auth.token,
Reiter, Christoph's avatar
Reiter, Christoph committed
                .then((result) => {
                    if (!result.ok) throw result;
                    return result.json();
                })
                .then((place) => {
                    that.object = place;
                    const transformed = that.jsonld.compactMember(
                        that.jsonld.expandMember(place),
                        checkInPlaceContext
                    );
                    const identifier = transformed['@id'];
                    const maxCapacity = transformed['maximumPhysicalAttendeeCapacity'];
                    const roomName = transformed['name'];
                    const room = place.identifier;

                    const option = new Option(
                        that.generateOptionText(transformed),
                        identifier,
                        true,
                        true
                    );
                    $this.attr('data-object', JSON.stringify(place));
                    $this.data('object', place);
                    that.$select.append(option).trigger('change');

                    // fire a change event
                    that.dispatchEvent(
                        new CustomEvent('change', {
                            detail: {
                                value: identifier,
                                capacity: maxCapacity,
                                room: room,
                                name: roomName,
                            },
                            bubbles: true,
                        })
                    );
                })
                .catch((e) => {
                    console.log(e);
                    that.clear();
                });
        }

        return true;
    }

    generateOptionText(place) {
Reiter, Christoph's avatar
Reiter, Christoph committed
        let text = place['name'];
        // add maximum capacity to checkInPlace if present
Reiter, Christoph's avatar
Reiter, Christoph committed
        if (
            this.showCapacity &&
            place['maximumPhysicalAttendeeCapacity'] !== undefined &&
            place['maximumPhysicalAttendeeCapacity'] !== null
        ) {
            let capacity = place['maximumPhysicalAttendeeCapacity'];
            text += ` (${capacity})`;
        }

        return text;
    }

    update(changedProperties) {
        changedProperties.forEach((oldValue, propName) => {
            switch (propName) {
Reiter, Christoph's avatar
Reiter, Christoph committed
                case 'lang':

                    if (this.select2IsInitialized()) {
                        // no other way to set an other language at runtime did work
                        this.initSelect2(true);
                    }
                    break;
Reiter, Christoph's avatar
Reiter, Christoph committed
                case 'value':
                    if (!this.ignoreValueUpdate && this.select2IsInitialized()) {
                        this.initSelect2();
                    }

                    this.ignoreValueUpdate = false;
                    break;
Reiter, Christoph's avatar
Reiter, Christoph committed
                case 'entryPointUrl':
                    // we don't need to preset the selector if the entry point url changes
                    this.initJSONLD(true);
                    break;
            }
        });

        super.update(changedProperties);
    }

    select2IsInitialized() {
Reiter, Christoph's avatar
Reiter, Christoph committed
        return this.$select !== null && this.$select.hasClass('select2-hidden-accessible');
    }

    reloadClick() {
        if (this.object === null) {
            return;
        }

        // fire a change event
Reiter, Christoph's avatar
Reiter, Christoph committed
        this.dispatchEvent(
            new CustomEvent('change', {
                detail: {
                    value: this.value,
                },
                bubbles: true,
            })
        );
    }

    static get styles() {
        return [
            commonStyles.getThemeCSS(),
            commonStyles.getGeneralCSS(),
            commonStyles.getButtonCSS(),
            commonStyles.getFormAddonsCSS(),
            commonStyles.getSelect2CSS(),
            css`
Reiter, Christoph's avatar
Reiter, Christoph committed
                .select2-control.control {
                    width: 100%;
                }
Reiter, Christoph's avatar
Reiter, Christoph committed
                .field .button.control {
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    border: var(--dbp-border);
                    border-color: var(--dbp-muted);
Reiter, Christoph's avatar
Reiter, Christoph committed
                    -moz-border-radius-topright: var(--dbp-border-radius);
                    -moz-border-radius-bottomright: var(--dbp-border-radius);
                    line-height: 100%;
                }
Reiter, Christoph's avatar
Reiter, Christoph committed
                .field .button.control dbp-icon {
                    top: 0;
                }
Reiter, Christoph's avatar
Reiter, Christoph committed
                input {
                    border-radius: 0;
                }
Reiter, Christoph's avatar
Reiter, Christoph committed
                input[type='search'] {
                    -webkit-appearance: none;
                }

                /* https://github.com/select2/select2/issues/5457 */
                .select2-bug .loading-results {
                    display: none !important;
                }
                
                .select2-container--default .select2-selection--single, .select2-dropdown, .select2-container--default .select2-search--dropdown .select2-search__field{
                    background: var(--dbp-background);
                    border: var(--dbp-border);
                    border-color: var(--dbp-muted);
                }
                
                .select2-dropdown{
                    background-color: var(--dbp-background);
                }
                
Reiter, Christoph's avatar
Reiter, Christoph committed
            `,
        ];
    }

    render() {
        const select2CSS = commonUtils.getAssetURL(select2CSSPath);
        return html`
Reiter, Christoph's avatar
Reiter, Christoph committed
            <link rel="stylesheet" href="${select2CSS}" />
            <style>
                #${this.selectId} {
                    width: 100%;
                }
            </style>

            <div class="select">
                <div class="field has-addons">
                    <div class="select2-control control">
                        <!-- https://select2.org-->
Reiter, Christoph's avatar
Reiter, Christoph committed
                        <select
                            id="${this.selectId}"
                            name="check-in-place"
                            class="select"
                            ?disabled=${!this.active}>
                            ${!this.active
                                ? html`
                                      <option value="" disabled selected>
                                          ${i18n.t('check-in-place-select.login-required')}
                                      </option>
                                  `
                                : ''}
                        </select>
Reiter, Christoph's avatar
Reiter, Christoph committed
                    <a
                        class="control button"
                        id="reload-button"
                        ?disabled=${this.object === null}
                        @click="${this.reloadClick}"
                        style="display: ${this.showReloadButton ? 'flex' : 'none'}"
                        title="${this.reloadButtonTitle}">
                        <dbp-icon name="reload"></dbp-icon>
                    </a>
                </div>
                <div id="check-in-place-select-dropdown"></div>