UNPKG

@legumeinfo/web-components

Version:

Web Components for the Legume Information System and other AgBio databases

637 lines 25.1 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { LitElement, css, html } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { live } from 'lit/directives/live.js'; import { createRef, ref } from 'lit/directives/ref.js'; import { LisCancelPromiseController } from './controllers'; import { LisPaginatedSearchMixin, } from './mixins'; /** * @htmlElement `<lis-pangene-lookup-element>` * * A Web Component that provides an interface for looking up pangenes and * displaying results in a view table. The component uses the * {@link mixins!LisPaginatedSearchMixin | `LisPaginatedSearchMixin`} mixin. See the * mixin docs for further details. * * @example * {@link !HTMLElement | `HTMLElement`} properties can only be set via * JavaScript. This means the {@link searchFunction | `searchFunction`} property * must be set on a `<lis-pangene-lookup-element>` tag's instance of the * {@link LisPangeneLookupElement | `LisPangeneLookupElement`} class. For example: * ```html * <!-- add the Web Component to your HTML --> * <lis-pangene-lookup-element id="pangene-lookup"></lis-pangene-lookup-element> * * <!-- configure the Web Component via JavaScript --> * <script type="text/javascript"> * // a site-specific function that sends a request to a pangene lookup API * function getPangenes(lookupData, page, {abortSignal}) { * // returns a Promise that resolves to a lookup result object * } * // get the pangene lookup element * const lookupElement = document.getElementById('pangene-lookup'); * // set the element's searchFunction property * lookupElement.searchFunction = getPangenes; * </script> * ``` * * @example * Data must be provided for the genus, species, strain, assembly, and annotation selectors in the * lookup form. This can be done by setting the form's {@link formData | `formData`} * attribute/property directly or by setting the * {@link formDataFunction | `formDataFunction`} property. Setting the latter will call * the function immediately and set the {@link formData | `formData`} value using the * result. For example: * ```html * <!-- add the Web Component to your HTML --> * <lis-pangene-lookup-element id="pangene-lookup"></lis-pangene-lookup-element> * * <!-- configure the Web Component via JavaScript --> * <script type="text/javascript"> * // a site-specific function that gets genus, species, strain, assembly, and annotation data from an API * function getPangeneFormData() { * // returns a Promise that resolves to a form data object * } * // get the pangene looktup element * const lookupElement = document.getElementById('pangene-lookup'); * // set the element's formDataFunction property * lookupElement.formDataFunction = getPangeneFormData; * </script> * ``` * * @example * The {@link genus | `genus`}, {@link species | `species`}, {@link strain | `strain`}, * {@link assembly | `assembly`}, and {@link annotation | `annotation`} properties can be used to * limit all lookups to a specific genus, species, strain, assembly, and annotation. This will cause * the genus, species, strain, assembly, and annotation fields of the lookup form to be * automatically set and disabled so that users cannot change them. * For example: * ```html * <!-- restrict the genus via HTML --> * <lis-pangene-lookup-element genus="Glycine"></lis-pangene-lookup-element> * * <!-- restrict the genus and species via HTML --> * <lis-pangene-lookup-element genus="Glycine" species="max"></lis-pangene-lookup-element> * * <!-- restrict the genus and species via JavaScript --> * <lis-pangene-lookup-element id="pangene-lookup"></lis-pangene-lookup-element> * * <script type="text/javascript"> * // get the pangene lookup element * const lookupElement = document.getElementById('pangene-lookup'); * // set the element's genus and species properties * lookupElement.genus = "Cicer"; * lookupElement.species = "arietinum"; * </script> * ``` * * @example * The {@link genesExample | `genesExample`} property can be used to set the example text for the * gene identifiers input field. For example: * ```html * <!-- set the example text via HTML --> * <lis-pangene-lookup-element genesExample="Glyma.13G357700 Glyma.13G357702"></lis-pangene-lookup-element> * * <!-- set the example text via JavaScript --> * <lis-pangene-lookup-element id="pangene-lookup"></lis-pangene-lookup-element> * * <script type="text/javascript"> * // get the pangene lookup element * const lookupElement = document.getElementById('pangene-lookup'); * // set the element's example text properties * lookupElement.genesExample = 'Glyma.13G357700 Glyma.13G357702'; * </script> * ``` */ let LisPangeneLookupElement = class LisPangeneLookupElement extends LisPaginatedSearchMixin(LitElement)() { _splitGenesFunctionWrapper(fn) { return (data, options) => { // @ts-expect-error Property 'trim' does not exist on type 'string[]' const genes = data['genes'].trim().split(this.genesRegexp); const modifiedData = { ...data, genes }; return fn(modifiedData, options); }; } willUpdate(changedProperties) { if (changedProperties.has('searchFunction')) { // @ts-expect-error incompatible types this.searchFunction = this._splitGenesFunctionWrapper(this.searchFunction); } if (changedProperties.has('downloadFunction') && this.downloadFunction !== undefined) { // @ts-expect-error incompatible types this.downloadFunction = this._splitGenesFunctionWrapper(this.downloadFunction); } } constructor() { super(); /** * The data used to construct the lookup form in the template. * * @attribute */ this.formData = { genuses: [] }; /** * An optional property that can be used to load the form data via an external function. * If used, the `formData` attribute/property will be updated using the result. */ this.formDataFunction = () => Promise.reject(new Error('No form data function provided')); /** * What regular experssion should be used to parse the input gene identifiers. * * @attribute */ this.genesRegexp = /\s+/; /** * The maximum number of input gene identifiers. * Warning: setting this number too high can cause queries to hit web browsers' URL size limit. * * @attribute */ this.genesLimit = 100; // the selected index of the genus select element this.selectedGenus = 0; // the selected index of the species select element this.selectedSpecies = 0; // the selected index of the strain select element this.selectedStrain = 0; // the selected index of the assembly select element this.selectedAssembly = 0; // the selected index of the annotation select element this.selectedAnnotation = 0; // a controller that allows in-flight form data requests to be cancelled this.formDataCancelPromiseController = new LisCancelPromiseController(this); // bind to the loading element in the template this._formLoadingRef = createRef(); this.queryStringReflection = false; this.resultAttributes = ['input', 'panGeneSet', 'target']; this.tableHeader = { input: 'Input', panGeneSet: 'PanGene Set', target: 'Target', }; this.tableColumnClasses = { description: 'uk-table-expand', }; } // called after every component update, e.g. when a property changes updated(changedProperties) { // call the formDataFunction every time its value changes if (changedProperties.has('formDataFunction')) { this._getFormData(); } // use querystring parameters to update the selectors when the form data changes if (changedProperties.has('formData') || changedProperties.has('genus') || changedProperties.has('species') || changedProperties.has('strain') || changedProperties.has('assembly') || changedProperties.has('annotation')) { this._initializeSelections(); } } // gets the data for the lookup form _getFormData() { var _a; // update the loading element (_a = this._formLoadingRef.value) === null || _a === void 0 ? void 0 : _a.loading(); // make the form data function cancellable this.formDataCancelPromiseController.cancel(); const options = { abortSignal: this.formDataCancelPromiseController.abortSignal, }; const formDataPromise = this.formDataFunction(options); // call the cancellable function this.formDataCancelPromiseController.wrapPromise(formDataPromise).then((formData) => { var _a; (_a = this._formLoadingRef.value) === null || _a === void 0 ? void 0 : _a.success(); this.formData = formData; }, (error) => { var _a; // do nothing if the request was aborted if (!(error instanceof Event && error.type === 'abort')) { (_a = this._formLoadingRef.value) === null || _a === void 0 ? void 0 : _a.failure(); throw error; } }); } // sets the selected indexes based on properties and querystring parameters async _initializeSelections() { this.selectedGenus = 0; this.selectedSpecies = 0; this.selectedStrain = 0; this.selectedAssembly = 0; this.selectedAnnotation = 0; } // called when a genus is selected _selectGenus(event) { if (event.target != null) { this.selectedGenus = event.target.selectedIndex; this.selectedSpecies = 0; this.selectedStrain = 0; this.selectedAssembly = 0; this.selectedAnnotation = 0; } } // renders the genus selector _renderGenusSelector() { const options = this.formData.genuses.map(({ genus }) => { return html `<option value="${genus}">${genus}</option>`; }); // HACK: the disabled attribute can't be set via template literal... if (this.genus !== undefined) { const value = this.selectedGenus ? this.formData.genuses[this.selectedGenus - 1].genus : ''; return html ` <select required class="uk-select uk-form-small" disabled .selectedIndex=${live(this.selectedGenus)} @change="${this._selectGenus}" > <option value="">-- select one --</option> ${options} </select> <input type="hidden" name="genus" value="${value}" /> `; } return html ` <select required class="uk-select uk-form-small" name="genus" .selectedIndex=${live(this.selectedGenus)} @change="${this._selectGenus}" > <option value="">-- select one --</option> ${options} </select> `; } // called when a species is selected _selectSpecies(event) { if (event.target != null) { this.selectedSpecies = event.target.selectedIndex; this.selectedStrain = 0; this.selectedAssembly = 0; this.selectedAnnotation = 0; } } // renders the species selector _renderSpeciesSelector() { let options = [html ``]; if (this.selectedGenus) { options = this.formData.genuses[this.selectedGenus - 1].species.map(({ species }) => { return html `<option value="${species}">${species}</option>`; }); } // HACK: the disabled attribute can't be set via template literal... if (this.genus !== undefined && this.species !== undefined) { const value = this.selectedGenus && this.selectedSpecies ? this.formData.genuses[this.selectedGenus - 1].species[this.selectedSpecies - 1].species : ''; return html ` <select class="uk-select uk-form-small" disabled .selectedIndex=${live(this.selectedSpecies)} @change="${this._selectSpecies}" > <option value="">-- any --</option> ${options} </select> <input type="hidden" name="species" value="${value}" /> `; } return html ` <select class="uk-select uk-form-small" name="species" .selectedIndex=${live(this.selectedSpecies)} @change="${this._selectSpecies}" > <option value="">-- any --</option> ${options} </select> `; } // called when an strain is selected _selectStrain(event) { if (event.target != null) { this.selectedStrain = event.target.selectedIndex; } } // renders the strain selector _renderStrainSelector() { let options = [html ``]; if (this.selectedGenus && this.selectedSpecies) { options = this.formData.genuses[this.selectedGenus - 1].species[this.selectedSpecies - 1].strains.map(({ strain }) => { return html `<option value="${strain}">${strain}</option>`; }); } // HACK: the disabled attribute can't be set via template literal... if (this.genus !== undefined && this.species !== undefined && this.strain !== undefined) { const value = this.selectedGenus && this.selectedSpecies && this.selectedStrain ? this.formData.genuses[this.selectedGenus - 1].species[this.selectedSpecies - 1].strains : ''; return html ` <select class="uk-select uk-form-small" disabled .selectedIndex=${live(this.selectedStrain)} @change="${this._selectStrain}" > <option value="">-- any --</option> ${options} </select> <input type="hidden" name="strain" value="${value}" /> `; } return html ` <select class="uk-select uk-form-small" name="strain" .selectedIndex=${live(this.selectedStrain)} @change="${this._selectStrain}" > <option value="">-- any --</option> ${options} </select> `; } // called when an assembly is selected _selectAssembly(event) { if (event.target != null) { this.selectedAssembly = event.target.selectedIndex; this.selectedAnnotation = 0; } } // renders the assembly selector _renderAssemblySelector() { let options = [html ``]; if (this.selectedGenus && this.selectedSpecies && this.selectedStrain) { options = this.formData.genuses[this.selectedGenus - 1].species[this.selectedSpecies - 1].strains[this.selectedStrain - 1].assemblies.map(({ assembly }) => { return html `<option value="${assembly}">${assembly}</option>`; }); } // HACK: the disabled attribute can't be set via template literal... if (this.genus !== undefined && this.species !== undefined && this.strain !== undefined && this.assembly !== undefined) { const value = this.selectedGenus && this.selectedSpecies && this.selectedStrain && this.selectedAssembly ? this.formData.genuses[this.selectedGenus - 1].species[this.selectedSpecies - 1].strains[this.selectedStrain - 1].assemblies : ''; return html ` <select class="uk-select uk-form-small" disabled .selectedIndex=${live(this.selectedAssembly)} @change="${this._selectAssembly}" > <option value="">-- any --</option> ${options} </select> <input type="hidden" name="assembly" value="${value}" /> `; } return html ` <select class="uk-select uk-form-small" name="assembly" .selectedIndex=${live(this.selectedAssembly)} @change="${this._selectAssembly}" > <option value="">-- any --</option> ${options} </select> `; } // called when an annotation is selected _selectAnnotation(event) { if (event.target != null) { this.selectedAnnotation = event.target.selectedIndex; } } // renders the annotation selector _renderAnnotationSelector() { let options = [html ``]; if (this.selectedGenus && this.selectedSpecies && this.selectedStrain && this.selectedAssembly) { options = this.formData.genuses[this.selectedGenus - 1].species[this.selectedSpecies - 1].strains[this.selectedStrain - 1].assemblies[this.selectedAssembly - 1].annotations.map(({ annotation }) => { return html `<option value="${annotation}">${annotation}</option>`; }); } // HACK: the disabled attribute can't be set via template literal... if (this.genus !== undefined && this.species !== undefined && this.strain !== undefined && this.assembly !== undefined && this.annotation !== undefined) { const value = this.selectedGenus && this.selectedSpecies && this.selectedStrain && this.selectedAssembly && this.selectedAnnotation ? this.formData.genuses[this.selectedGenus - 1].species[this.selectedSpecies - 1].strains[this.selectedStrain - 1].assemblies[this.selectedAssembly - 1].annotations : ''; return html ` <select class="uk-select uk-form-small" disabled .selectedIndex=${live(this.selectedAnnotation)} @change="${this._selectAnnotation}" > <option value="">-- any --</option> ${options} </select> <input type="hidden" name="annotation" value="${value}" /> `; } return html ` <select class="uk-select uk-form-small" name="annotation" .selectedIndex=${live(this.selectedAnnotation)} @change="${this._selectAnnotation}" > <option value="">-- any --</option> ${options} </select> `; } // called when the form is submitted to run custom field validation _validateForm(e) { const formElement = e.target; if (formElement == null) return; // check genes textarea validity const genesElement = formElement.genes; const identifiers = genesElement.value.trim().split(this.genesRegexp); let genesValidity = ''; if (identifiers.length > this.genesLimit) { genesValidity = `No more than ${this.genesLimit} gene identifiers allowed.`; } genesElement.setCustomValidity(genesValidity); // check form validity; will catch standard and custom invalid fields if (!formElement.checkValidity()) { e.preventDefault(); e.stopPropagation(); formElement.reportValidity(); } } /** @ignore */ // used by LisPaginatedSearchMixin to draw the lookup form part of template renderForm() { // render the form's selectors const genusSelector = this._renderGenusSelector(); const speciesSelector = this._renderSpeciesSelector(); const strainSelector = this._renderStrainSelector(); const assemblySelector = this._renderAssemblySelector(); const annotationSelector = this._renderAnnotationSelector(); // render the optional download button let downloadButton = html ``; if (this.downloadFunction !== undefined) { downloadButton = html ` <button type="submit" value="download" class="uk-button uk-button-default" > Download </button> <lis-inline-loading-element ${ref(this._downloadingRef)} ></lis-inline-loading-element> `; } // render the form return html ` <form class="uk-form-stacked" novalidate @submit="${this._validateForm}"> <fieldset class="uk-fieldset"> <legend class="uk-legend">Pangene Lookup</legend> <lis-loading-element ${ref(this._formLoadingRef)} ></lis-loading-element> <div class="uk-margin uk-grid-small" uk-grid> <div class="uk-width-1-1@s"> <label class="uk-form-label" for="identifier" >Gene Identifiers</label > <textarea required class="uk-textarea" rows="5" name="genes" ></textarea> <lis-form-input-example-element .text=${this.genesExample} ></lis-form-input-example-element> </div> </div> <label class="uk-form-label" >Constraints target pangenes must satisfy</label > <div class="uk-margin uk-grid-small" uk-grid> <div class="uk-width-1-3@s"> <label class="uk-form-label" for="genus">Genus</label> ${genusSelector} </div> <div class="uk-width-1-3@s"> <label class="uk-form-label" for="species">Species</label> ${speciesSelector} </div> <div class="uk-width-1-3@s"> <label class="uk-form-label" for="strain">Strain</label> ${strainSelector} </div> </div> <div class="uk-margin uk-grid-small" uk-grid> <div class="uk-width-1-3@s"> <label class="uk-form-label" for="assembly">Assembly</label> ${assemblySelector} </div> <div class="uk-width-1-3@s"> <label class="uk-form-label" for="annotation">Annotation</label> ${annotationSelector} </div> </div> <div class="uk-margin"> <button type="submit" class="uk-button uk-button-primary"> Lookup </button> ${downloadButton} </div> </fieldset> </form> `; } }; /** @ignore */ // used by Lit to style the Shadow DOM // not necessary but exclusion breaks TypeDoc LisPangeneLookupElement.styles = css ``; __decorate([ property() ], LisPangeneLookupElement.prototype, "formData", void 0); __decorate([ property({ type: Function, attribute: false }) ], LisPangeneLookupElement.prototype, "formDataFunction", void 0); __decorate([ property({ type: String }) ], LisPangeneLookupElement.prototype, "genus", void 0); __decorate([ property({ type: String }) ], LisPangeneLookupElement.prototype, "species", void 0); __decorate([ property({ type: String }) ], LisPangeneLookupElement.prototype, "strain", void 0); __decorate([ property({ type: String }) ], LisPangeneLookupElement.prototype, "assembly", void 0); __decorate([ property({ type: String }) ], LisPangeneLookupElement.prototype, "annotation", void 0); __decorate([ property({ type: String }) ], LisPangeneLookupElement.prototype, "genesExample", void 0); __decorate([ property({ type: RegExp }) ], LisPangeneLookupElement.prototype, "genesRegexp", void 0); __decorate([ property({ type: Number }) ], LisPangeneLookupElement.prototype, "genesLimit", void 0); __decorate([ state() ], LisPangeneLookupElement.prototype, "selectedGenus", void 0); __decorate([ state() ], LisPangeneLookupElement.prototype, "selectedSpecies", void 0); __decorate([ state() ], LisPangeneLookupElement.prototype, "selectedStrain", void 0); __decorate([ state() ], LisPangeneLookupElement.prototype, "selectedAssembly", void 0); __decorate([ state() ], LisPangeneLookupElement.prototype, "selectedAnnotation", void 0); LisPangeneLookupElement = __decorate([ customElement('lis-pangene-lookup-element') ], LisPangeneLookupElement); export { LisPangeneLookupElement }; //# sourceMappingURL=lis-pangene-lookup-element.js.map