Skip to content
Snippets Groups Projects
Commit 1a9727d5 authored by Reiter, Christoph's avatar Reiter, Christoph :snake:
Browse files

Port to scoped elements

parent c61e117f
No related branches found
No related tags found
No related merge requests found
{
"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"
......
......@@ -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/*'
......
import {PersonSelect} from './person-select.js';
export {PersonSelect};
\ No newline at end of file
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
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 },
......
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);
auth @ b9fbe487
Subproject commit c315a49da7ef1931e319d3a8ead376eaf89cd36e
Subproject commit b9fbe487a8a6117ae90a423a8ba318a0321bf51b
common @ 9c6dc1fd
Subproject commit 5aa64ec47e7b65d69327f4dec1102917fe33bd22
Subproject commit 9c6dc1fd5e004eba32e31adf8e4485a26c345fe0
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment