diff --git a/packages/person-select/package.json b/packages/person-select/package.json index 6434f80ab493fa94e987a120160ee965817a595e..90aafa9883996b8e3936ad38bd41c958690124a5 100644 --- a/packages/person-select/package.json +++ b/packages/person-select/package.json @@ -1,7 +1,7 @@ { "name": "vpu-person-select", "version": "1.0.0", - "main": "src/vpu-person-select.js", + "main": "src/index.js", "devDependencies": { "chai": "^4.2.0", "i18next-scanner": "^2.10.2", @@ -26,6 +26,7 @@ "vpu-common": "file:./vendor/common" }, "dependencies": { + "@open-wc/scoped-elements": "^1.0.9", "jquery": "^3.4.1", "lit-element": "^2.1.0", "select2": "^4.0.10" diff --git a/packages/person-select/rollup.config.js b/packages/person-select/rollup.config.js index 7455772c07ab184acb8cfea80cbb221c9be183dd..4cc3a9bce151f67a39e5abb47859230e2cc351e6 100644 --- a/packages/person-select/rollup.config.js +++ b/packages/person-select/rollup.config.js @@ -22,6 +22,13 @@ export default { format: 'esm', sourcemap: true }, + onwarn: function (warning, warn) { + // keycloak bundled code uses eval + if (warning.code === 'EVAL') { + return; + } + warn(warning); + }, plugins: [ del({ targets: 'dist/*' diff --git a/packages/person-select/src/index.js b/packages/person-select/src/index.js new file mode 100644 index 0000000000000000000000000000000000000000..776fa475b7f2f3ffc747cbe24194838f65fca3b2 --- /dev/null +++ b/packages/person-select/src/index.js @@ -0,0 +1,3 @@ +import {PersonSelect} from './person-select.js'; + +export {PersonSelect}; \ No newline at end of file diff --git a/packages/person-select/src/person-select.js b/packages/person-select/src/person-select.js new file mode 100644 index 0000000000000000000000000000000000000000..8c49d9c5c4fd7a9f1804531e7e96a1d854401706 --- /dev/null +++ b/packages/person-select/src/person-select.js @@ -0,0 +1,362 @@ +import $ from 'jquery'; +import {findObjectInApiResults} from './utils.js'; +import select2 from 'select2'; +import select2LangDe from './i18n/de/select2' +import select2LangEn from './i18n/en/select2' +import JSONLD from 'vpu-common/jsonld'; +import {css, html, LitElement} from 'lit-element'; +import {ScopedElementsMixin} from '@open-wc/scoped-elements'; +import {i18n} from './i18n.js'; +import {Icon} from 'vpu-common'; +import * as commonUtils from 'vpu-common/utils'; +import * as commonStyles from 'vpu-common/styles'; +import select2CSSPath from 'select2/dist/css/select2.min.css'; +import * as errorUtils from "vpu-common/error"; + + +const personContext = { + "@id": "@id", + "name": "http://schema.org/name", + "birthDate": "http://schema.org/Date" +}; + +select2(window, $); + +export class PersonSelect extends ScopedElementsMixin(LitElement) { + + constructor() { + super(); + this.lang = 'de'; + this.entryPointUrl = commonUtils.getAPiUrl(); + 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 = 'person-select-' + commonUtils.makeId(24); + this.value = ''; + this.object = null; + this.ignoreValueUpdate = false; + this.isSearching = false; + this.lastResult = {}; + this.showReloadButton = false; + this.reloadButtonTitle = ''; + this.showBirthDate = false; + } + + static get scopedElements() { + return { + 'vpu-icon': Icon, + }; + } + + $(selector) { + return $(this.shadowRoot.querySelector(selector)); + } + + static get properties() { + return { + 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' }, + showBirthDate: { type: Boolean, attribute: 'show-birth-date' }, + }; + } + + clear() { + this.object = null; + $(this).attr("data-object", ""); + $(this).data("object", null); + this.$select.val(null).trigger('change').trigger('select2:unselect'); + } + + connectedCallback() { + super.connectedCallback(); + const that = this; + + this.updateComplete.then(()=>{ + that.$select = that.$('#' + that.selectId); + + // close the selector on blur of the web component + $(that).blur(() => { + // the 500ms delay is a workaround to actually get an item selected when clicking on it, + // because the blur gets also fired when clicking in the selector + setTimeout(() => { + if (this.select2IsInitialized()) { + that.$select.select2('close'); + } + }, 500); + }); + + // try an init when user-interface is loaded + that.initJSONLD(); + }); + } + + initJSONLD(ignorePreset = false) { + const that = this; + + JSONLD.initialize(this.entryPointUrl, function (jsonld) { + that.jsonld = jsonld; + that.active = true; + + // 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 + */ + initSelect2(ignorePreset = false) { + const that = this; + const $this = $(this); + + if (this.jsonld === null) { + return false; + } + + // find the correct api url for a person + const apiUrl = this.jsonld.getApiUrlForIdentifier("http://schema.org/Person"); + // const apiUrl = this.jsonld.getApiUrlForEntityName("Event"); + + 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'); + } + + this.$select.select2({ + width: '100%', + language: this.lang === "de" ? select2LangDe() : select2LangEn(), + minimumInputLength: 2, + placeholder: i18n.t('person-select.placeholder'), + dropdownParent: this.$('#person-select-dropdown'), + ajax: { + delay: 500, + url: apiUrl, + contentType: "application/ld+json", + beforeSend: function (jqXHR) { + jqXHR.setRequestHeader('Authorization', 'Bearer ' + window.VPUAuthToken); + that.isSearching = true; + }, + data: function (params) { + return { + search: params.term.trim(), + }; + }, + processResults: function (data) { + that.lastResult = data; + let transformed = that.jsonld.transformMembers(data, personContext); + const results = []; + transformed.forEach((person) => { + results.push({id: person["@id"], text: that.generateOptionText(person)}); + }); + + return { + results: results + }; + }, + error: errorUtils.handleXhrError, + complete: (jqXHR, textStatus) => { + that.isSearching = false; + } + } + }).on("select2:select", function (e) { + // set custom element attributes + const identifier = e.params.data.id; + that.object = findObjectInApiResults(identifier, that.lastResult); + + $this.attr("data-object", JSON.stringify(that.object)); + $this.data("object", that.object); + + if ($this.attr("value") !== identifier) { + that.ignoreValueUpdate = true; + $this.attr("value", identifier); + + // fire a change event + that.dispatchEvent(new CustomEvent('change', { + detail: { + value: identifier, + }, + 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 person + if (!ignorePreset && this.value !== '') { + const apiUrl = this.entryPointUrl + this.value; + + fetch(apiUrl, { + headers: { + 'Content-Type': 'application/ld+json', + 'Authorization': 'Bearer ' + window.VPUAuthToken, + }, + }) + .then(result => { + if (!result.ok) throw result; + return result.json(); + }) + .then((person) => { + that.object = person; + const transformed = that.jsonld.compactMember(that.jsonld.expandMember(person), personContext); + const identifier = transformed["@id"]; + + const option = new Option(that.generateOptionText(transformed), identifier, true, true); + $this.attr("data-object", JSON.stringify(person)); + $this.data("object", person); + that.$select.append(option).trigger('change'); + + // fire a change event + that.dispatchEvent(new CustomEvent('change', { + detail: { + value: identifier, + }, + bubbles: true + })); + }).catch((e) => { + console.log(e); + that.clear(); + }); + } + + return true; + } + + generateOptionText(person) { + let text = person["name"]; + + // add birth date to name if present + if (this.showBirthDate && (person["birthDate"] !== undefined) && (person["birthDate"] !== null)) { + const date = new Date(person["birthDate"]); + text += ` (${date.toLocaleDateString("de-AT")})`; + } + + return text; + } + + update(changedProperties) { + changedProperties.forEach((oldValue, propName) => { + switch (propName) { + case "lang": + i18n.changeLanguage(this.lang); + + if (this.select2IsInitialized()) { + // no other way to set an other language at runtime did work + this.initSelect2(true); + } + break; + case "value": + if (!this.ignoreValueUpdate && this.select2IsInitialized()) { + this.initSelect2(); + } + + this.ignoreValueUpdate = false; + break; + case "entryPointUrl": + // we don't need to preset the selector if the entry point url changes + this.initJSONLD(true); + break; + } + }); + + super.update(changedProperties); + } + + select2IsInitialized() { + return this.$select !== null && this.$select.hasClass("select2-hidden-accessible"); + } + + reloadClick() { + if (this.object === null) { + return; + } + + // fire a change event + this.dispatchEvent(new CustomEvent('change', { + detail: { + value: this.value, + }, + bubbles: true + })); + } + + static get styles() { + // language=css + return css` + ${commonStyles.getThemeCSS()} + ${commonStyles.getGeneralCSS()} + ${commonStyles.getButtonCSS()} + ${commonStyles.getFormAddonsCSS()} + ${commonStyles.getSelect2CSS()} + + .select2-control.control { + width: 100%; + } + + .field .button.control { + display: flex; + align-items: center; + justify-content: center; + border: 1px solid #aaa; + -moz-border-radius-topright: var(--vpu-border-radius); + -moz-border-radius-bottomright: var(--vpu-border-radius); + line-height: 100%; + } + + .field .button.control vpu-icon { + top: 0; + } + `; + } + + render() { + commonUtils.initAssetBaseURL('vpu-person-select-src'); + const select2CSS = commonUtils.getAssetURL(select2CSSPath); + return html` + <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--> + <select id="${this.selectId}" name="person" class="select" ?disabled=${!this.active}>${!this.active ? html`<option value="" disabled selected>${ i18n.t('person-select.login-required')}</option>` : ''}</select> + </div> + <a class="control button" + id="reload-button" + ?disabled=${this.object === null} + @click="${this.reloadClick}" + style="display: ${this.showReloadButton ? "flex" : "none"}" + title="${this.reloadButtonTitle}"> + <vpu-icon name="reload"></vpu-icon> + </a> + </div> + <div id="person-select-dropdown"></div> + </div> + `; + } +} \ No newline at end of file diff --git a/packages/person-select/src/vpu-person-select-demo.js b/packages/person-select/src/vpu-person-select-demo.js index a698806d7d65918fcffe1c6ecf23048a72facf07..0f09156b3be7a9f85b90e5a9726131228c985a8a 100644 --- a/packages/person-select/src/vpu-person-select-demo.js +++ b/packages/person-select/src/vpu-person-select-demo.js @@ -1,17 +1,25 @@ -import 'vpu-auth'; import {i18n} from './i18n.js'; import {css, html, LitElement} from 'lit-element'; -import './vpu-person-select.js'; +import {ScopedElementsMixin} from '@open-wc/scoped-elements'; +import {PersonSelect} from './person-select.js'; +import {Auth} from 'vpu-auth'; import * as commonUtils from 'vpu-common/utils'; import * as commonStyles from 'vpu-common/styles'; -class PersonSelectDemo extends LitElement { +class PersonSelectDemo extends ScopedElementsMixin(LitElement) { constructor() { super(); this.lang = 'de'; this.noAuth = false; } + static get scopedElements() { + return { + 'vpu-auth': Auth, + 'vpu-person-select': PersonSelect, + }; + } + static get properties() { return { lang: { type: String }, diff --git a/packages/person-select/src/vpu-person-select.js b/packages/person-select/src/vpu-person-select.js index 602c84049668ca6442f59aa73250892d7985acca..a343963d000c520085183ba06b6edb9157e8d5a9 100644 --- a/packages/person-select/src/vpu-person-select.js +++ b/packages/person-select/src/vpu-person-select.js @@ -1,356 +1,4 @@ -import $ from 'jquery'; -import {findObjectInApiResults} from './utils.js'; -import select2 from 'select2'; -import select2LangDe from './i18n/de/select2' -import select2LangEn from './i18n/en/select2' -import JSONLD from 'vpu-common/jsonld'; -import {css, html, LitElement} from 'lit-element'; -import {i18n} from './i18n.js'; import * as commonUtils from 'vpu-common/utils'; -import * as commonStyles from 'vpu-common/styles'; -import select2CSSPath from 'select2/dist/css/select2.min.css'; -import * as errorUtils from "vpu-common/error"; - - -const personContext = { - "@id": "@id", - "name": "http://schema.org/name", - "birthDate": "http://schema.org/Date" -}; - -select2(window, $); - -class PersonSelect extends LitElement { - - constructor() { - super(); - this.lang = 'de'; - this.entryPointUrl = commonUtils.getAPiUrl(); - 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 = 'person-select-' + commonUtils.makeId(24); - this.value = ''; - this.object = null; - this.ignoreValueUpdate = false; - this.isSearching = false; - this.lastResult = {}; - this.showReloadButton = false; - this.reloadButtonTitle = ''; - this.showBirthDate = false; - } - - $(selector) { - return $(this.shadowRoot.querySelector(selector)); - } - - static get properties() { - return { - 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' }, - showBirthDate: { type: Boolean, attribute: 'show-birth-date' }, - }; - } - - clear() { - this.object = null; - $(this).attr("data-object", ""); - $(this).data("object", null); - this.$select.val(null).trigger('change').trigger('select2:unselect'); - } - - connectedCallback() { - super.connectedCallback(); - const that = this; - - this.updateComplete.then(()=>{ - that.$select = that.$('#' + that.selectId); - - // close the selector on blur of the web component - $(that).blur(() => { - // the 500ms delay is a workaround to actually get an item selected when clicking on it, - // because the blur gets also fired when clicking in the selector - setTimeout(() => { - if (this.select2IsInitialized()) { - that.$select.select2('close'); - } - }, 500); - }); - - // try an init when user-interface is loaded - that.initJSONLD(); - }); - } - - initJSONLD(ignorePreset = false) { - const that = this; - - JSONLD.initialize(this.entryPointUrl, function (jsonld) { - that.jsonld = jsonld; - that.active = true; - - // 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 - */ - initSelect2(ignorePreset = false) { - const that = this; - const $this = $(this); - - if (this.jsonld === null) { - return false; - } - - // find the correct api url for a person - const apiUrl = this.jsonld.getApiUrlForIdentifier("http://schema.org/Person"); - // const apiUrl = this.jsonld.getApiUrlForEntityName("Event"); - - 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'); - } - - this.$select.select2({ - width: '100%', - language: this.lang === "de" ? select2LangDe() : select2LangEn(), - minimumInputLength: 2, - placeholder: i18n.t('person-select.placeholder'), - dropdownParent: this.$('#person-select-dropdown'), - ajax: { - delay: 500, - url: apiUrl, - contentType: "application/ld+json", - beforeSend: function (jqXHR) { - jqXHR.setRequestHeader('Authorization', 'Bearer ' + window.VPUAuthToken); - that.isSearching = true; - }, - data: function (params) { - return { - search: params.term.trim(), - }; - }, - processResults: function (data) { - that.lastResult = data; - let transformed = that.jsonld.transformMembers(data, personContext); - const results = []; - transformed.forEach((person) => { - results.push({id: person["@id"], text: that.generateOptionText(person)}); - }); - - return { - results: results - }; - }, - error: errorUtils.handleXhrError, - complete: (jqXHR, textStatus) => { - that.isSearching = false; - } - } - }).on("select2:select", function (e) { - // set custom element attributes - const identifier = e.params.data.id; - that.object = findObjectInApiResults(identifier, that.lastResult); - - $this.attr("data-object", JSON.stringify(that.object)); - $this.data("object", that.object); - - if ($this.attr("value") !== identifier) { - that.ignoreValueUpdate = true; - $this.attr("value", identifier); - - // fire a change event - that.dispatchEvent(new CustomEvent('change', { - detail: { - value: identifier, - }, - 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 person - if (!ignorePreset && this.value !== '') { - const apiUrl = this.entryPointUrl + this.value; - - fetch(apiUrl, { - headers: { - 'Content-Type': 'application/ld+json', - 'Authorization': 'Bearer ' + window.VPUAuthToken, - }, - }) - .then(result => { - if (!result.ok) throw result; - return result.json(); - }) - .then((person) => { - that.object = person; - const transformed = that.jsonld.compactMember(that.jsonld.expandMember(person), personContext); - const identifier = transformed["@id"]; - - const option = new Option(that.generateOptionText(transformed), identifier, true, true); - $this.attr("data-object", JSON.stringify(person)); - $this.data("object", person); - that.$select.append(option).trigger('change'); - - // fire a change event - that.dispatchEvent(new CustomEvent('change', { - detail: { - value: identifier, - }, - bubbles: true - })); - }).catch((e) => { - console.log(e); - that.clear(); - }); - } - - return true; - } - - generateOptionText(person) { - let text = person["name"]; - - // add birth date to name if present - if (this.showBirthDate && (person["birthDate"] !== undefined) && (person["birthDate"] !== null)) { - const date = new Date(person["birthDate"]); - text += ` (${date.toLocaleDateString("de-AT")})`; - } - - return text; - } - - update(changedProperties) { - changedProperties.forEach((oldValue, propName) => { - switch (propName) { - case "lang": - i18n.changeLanguage(this.lang); - - if (this.select2IsInitialized()) { - // no other way to set an other language at runtime did work - this.initSelect2(true); - } - break; - case "value": - if (!this.ignoreValueUpdate && this.select2IsInitialized()) { - this.initSelect2(); - } - - this.ignoreValueUpdate = false; - break; - case "entryPointUrl": - // we don't need to preset the selector if the entry point url changes - this.initJSONLD(true); - break; - } - }); - - super.update(changedProperties); - } - - select2IsInitialized() { - return this.$select !== null && this.$select.hasClass("select2-hidden-accessible"); - } - - reloadClick() { - if (this.object === null) { - return; - } - - // fire a change event - this.dispatchEvent(new CustomEvent('change', { - detail: { - value: this.value, - }, - bubbles: true - })); - } - - static get styles() { - // language=css - return css` - ${commonStyles.getThemeCSS()} - ${commonStyles.getGeneralCSS()} - ${commonStyles.getButtonCSS()} - ${commonStyles.getFormAddonsCSS()} - ${commonStyles.getSelect2CSS()} - - .select2-control.control { - width: 100%; - } - - .field .button.control { - display: flex; - align-items: center; - justify-content: center; - border: 1px solid #aaa; - -moz-border-radius-topright: var(--vpu-border-radius); - -moz-border-radius-bottomright: var(--vpu-border-radius); - line-height: 100%; - } - - .field .button.control vpu-icon { - top: 0; - } - `; - } - - render() { - commonUtils.initAssetBaseURL('vpu-person-select-src'); - const select2CSS = commonUtils.getAssetURL(select2CSSPath); - return html` - <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--> - <select id="${this.selectId}" name="person" class="select" ?disabled=${!this.active}>${!this.active ? html`<option value="" disabled selected>${ i18n.t('person-select.login-required')}</option>` : ''}</select> - </div> - <a class="control button" - id="reload-button" - ?disabled=${this.object === null} - @click="${this.reloadClick}" - style="display: ${this.showReloadButton ? "flex" : "none"}" - title="${this.reloadButtonTitle}"> - <vpu-icon name="reload"></vpu-icon> - </a> - </div> - <div id="person-select-dropdown"></div> - </div> - `; - } -} +import {PersonSelect} from './person-select.js'; commonUtils.defineCustomElement('vpu-person-select', PersonSelect); diff --git a/packages/person-select/vendor/auth b/packages/person-select/vendor/auth index c315a49da7ef1931e319d3a8ead376eaf89cd36e..b9fbe487a8a6117ae90a423a8ba318a0321bf51b 160000 --- a/packages/person-select/vendor/auth +++ b/packages/person-select/vendor/auth @@ -1 +1 @@ -Subproject commit c315a49da7ef1931e319d3a8ead376eaf89cd36e +Subproject commit b9fbe487a8a6117ae90a423a8ba318a0321bf51b diff --git a/packages/person-select/vendor/common b/packages/person-select/vendor/common index 5aa64ec47e7b65d69327f4dec1102917fe33bd22..9c6dc1fd5e004eba32e31adf8e4485a26c345fe0 160000 --- a/packages/person-select/vendor/common +++ b/packages/person-select/vendor/common @@ -1 +1 @@ -Subproject commit 5aa64ec47e7b65d69327f4dec1102917fe33bd22 +Subproject commit 9c6dc1fd5e004eba32e31adf8e4485a26c345fe0