Source: src/js/views/searchSelect/BioontologySelectView.js

  1. "use strict";
  2. define([
  3. "backbone",
  4. "jquery",
  5. "semantic",
  6. "models/ontologies/BioontologyBatch",
  7. "views/searchSelect/SolrAutocompleteView",
  8. "views/ontologies/BioontologyBrowserView",
  9. ], (
  10. Backbone,
  11. $,
  12. Semantic,
  13. BioontologyBatch,
  14. SolrAutocompleteView,
  15. BioontologyBrowserView,
  16. ) => {
  17. // The base class for the view
  18. const BASE_CLASS = "bioontology-select";
  19. // The class names used in the view
  20. const CLASS_NAMES = {
  21. button: [
  22. Semantic.CLASS_NAMES.base,
  23. Semantic.CLASS_NAMES.button.base,
  24. Semantic.CLASS_NAMES.variations.attached,
  25. Semantic.CLASS_NAMES.colors.blue,
  26. "right",
  27. ],
  28. buttonIcon: ["icon", "icon-external-link-sign"],
  29. // Bootstrap classes
  30. modalCloseButton: "close",
  31. modal: ["modal", "hide", `${BASE_CLASS}-modal`],
  32. modalContent: "modal-body",
  33. };
  34. /**
  35. * @class BioontologySelectView
  36. * @classdesc A search select view that allows users to search for ontology
  37. * classes that are indexed in Solr and to browse BioPortal ontologies. The
  38. * view can be configured to show class labels from multiple ontologies.
  39. * @classcategory Views/SearchSelect
  40. * @augments SearchSelect
  41. * @since 2.31.0
  42. * @screenshot views/searchSelect/BioontologySelectView.png
  43. */
  44. return SolrAutocompleteView.extend(
  45. /** @lends BioontologySelectView.prototype */
  46. {
  47. /** @inheritdoc */
  48. type: "OntologySelect",
  49. /** @inheritdoc */
  50. className: `${BASE_CLASS} ${SolrAutocompleteView.prototype.className}`,
  51. /**
  52. * The name of the field in the Solr schema that the user is searching.
  53. * @type {string}
  54. */
  55. queryField: "sem_annotation",
  56. /**
  57. * Set this to false to avoid fetching class labels from BioPortal. The
  58. * labels will be displayed as the values that are returned from the Solr
  59. * query.
  60. * @type {boolean}
  61. */
  62. showClassLabels: true,
  63. /**
  64. * The ontologies that can be searched or browsed. Each ontology needs a
  65. * "label" and a "ontology" (acronym) property. Optionally, a "subTree"
  66. * property can be provided to search a specific sub-tree of the ontology.
  67. * @type {Array.<{label: string, ontology: string, subTree: string}>}
  68. * @since 2.31.0
  69. */
  70. ontologies: MetacatUI.appModel.get("bioportalOntologies"),
  71. /**
  72. * Initialize the view
  73. * @param {object} [opts] - The options to initialize the view with
  74. * @param {boolean} [opts.showClassLabels] - Set to false to avoid
  75. * fetching class labels from BioPortal
  76. * @param {string} [opts.queryField] - The name of the field in the Solr
  77. * schema that the user is searching
  78. * @param {object[]} [opts.ontologies] - The ontoloties (& sub-trees) to
  79. * allow users to search for.
  80. */
  81. initialize(opts = {}) {
  82. if (opts?.showClassLabels === false) this.showClassLabels = false;
  83. const attrs = opts || {};
  84. attrs.queryField = opts?.queryField || this.queryField;
  85. attrs.fluid = false;
  86. if (attrs.ontologies) {
  87. this.ontologies = attrs.ontologies;
  88. }
  89. attrs.submenuStyle = "accordion";
  90. attrs.allowAdditions = false;
  91. SolrAutocompleteView.prototype.initialize.call(this, attrs);
  92. if (this.showClassLabels) this.fetchClassLabels();
  93. },
  94. /** @inheritdoc */
  95. render() {
  96. SolrAutocompleteView.prototype.render.call(this);
  97. this.renderButton();
  98. this.renderOntologyModal();
  99. this.addListeners();
  100. return this;
  101. },
  102. /** Create the button to open the ontology browser */
  103. renderButton() {
  104. const button = document.createElement("button");
  105. button.classList.add(...CLASS_NAMES.button);
  106. const icon = document.createElement("i");
  107. icon.classList.add(...CLASS_NAMES.buttonIcon);
  108. icon.style.color = "inherit";
  109. button.appendChild(icon);
  110. this.el.appendChild(button);
  111. this.button = button;
  112. },
  113. /** Render the ontology browser modal */
  114. renderOntologyModal() {
  115. this.browser = new BioontologyBrowserView({
  116. ontologyOptions: this.ontologies,
  117. });
  118. // Bootstrap modal close button
  119. const closeButton = document.createElement("button");
  120. closeButton.classList.add(CLASS_NAMES.modalCloseButton);
  121. closeButton.setAttribute("data-dismiss", "modal");
  122. closeButton.setAttribute("aria-hidden", "true");
  123. closeButton.innerHTML = "&times;";
  124. const contentDiv = document.createElement("div");
  125. contentDiv.classList.add(CLASS_NAMES.modalContent);
  126. const modal = document.createElement("div");
  127. modal.classList.add(...CLASS_NAMES.modal);
  128. modal.append(contentDiv, closeButton);
  129. contentDiv.appendChild(this.browser.el);
  130. document.body.appendChild(modal);
  131. this.modal = $(modal).modal({ show: false });
  132. this.hideOntologyBrowser();
  133. },
  134. /**
  135. * Listen to when a class is selected in the browser & when the button is
  136. * clicked to open the browser
  137. */
  138. addListeners() {
  139. this.listenTo(this.browser, "selected", (cls) => {
  140. this.modal.modal("hide");
  141. this.selectClass(cls);
  142. });
  143. this.button.addEventListener("click", () => {
  144. this.showOntologyBrowser();
  145. });
  146. },
  147. /** Show the ontology browser modal */
  148. showOntologyBrowser() {
  149. // Don't render until the first time the modal is shown
  150. if (!this.browserRendered) {
  151. this.browser.render();
  152. this.browserRendered = true;
  153. }
  154. this.modal.modal("show");
  155. },
  156. /** Hide the ontology browser modal */
  157. hideOntologyBrowser() {
  158. this.modal.modal("hide");
  159. },
  160. /**
  161. * Set the value of the select element to the given ontology class
  162. * @param {OntologyClass} ontologyClass - The class model to select
  163. */
  164. selectClass(ontologyClass) {
  165. const option = ontologyClass.toSearchSelectOption();
  166. const selectedValue = option.value;
  167. this.model.addSelected(selectedValue);
  168. this.model.get("options").add(option);
  169. },
  170. /** Fetch the labels select element from BioPortal */
  171. async fetchClassLabels() {
  172. const options = this.model.get("options");
  173. if (!options.length) {
  174. this.listenToOnce(options, "add reset", this.fetchClassLabels);
  175. return;
  176. }
  177. const values = options.pluck("value");
  178. const preSelected = this.model.get("selected");
  179. const classesToFetch = [...values, ...preSelected];
  180. if (!MetacatUI.bioontologySearch) {
  181. MetacatUI.bioontologySearch = new BioontologyBatch();
  182. }
  183. this.bioBatchModel = MetacatUI.bioontologySearch;
  184. const ontologies = this.ontologies?.map(
  185. (ontology) => ontology.ontology,
  186. );
  187. this.bioBatchModel.set("ontologies", ontologies);
  188. this.listenTo(
  189. this.bioBatchModel.get("collection"),
  190. "update",
  191. (collection) => {
  192. this.addOptionDetails(collection);
  193. },
  194. );
  195. const allClasses = await this.bioBatchModel.getClasses(classesToFetch);
  196. this.addOptionDetails(allClasses);
  197. },
  198. /**
  199. * Add the details of the ontology classes to the options in the select
  200. * element
  201. * @param {Backbone.Collection|Array} classes - The collection of classes to add
  202. * to the options
  203. */
  204. addOptionDetails(classes) {
  205. if (!classes?.length) return;
  206. // if collection is an array, make it a collection
  207. let collection = classes;
  208. if (!classes.get) collection = new Backbone.Collection(collection);
  209. const options = this.model.get("options");
  210. // Get only the options that don't already at least have a label
  211. const toUpdate = options.filter((option) => {
  212. const label = option.get("label");
  213. const value = option.get("value");
  214. return !label || value === label;
  215. });
  216. toUpdate.forEach((option) => {
  217. const classId = option.get("value");
  218. const cls = collection.get(classId);
  219. if (cls) {
  220. const newAttrs = cls.toSearchSelectOption();
  221. const existingDescription = option.get("description");
  222. if (existingDescription) {
  223. newAttrs.description = `${newAttrs.description} (${existingDescription})`;
  224. }
  225. option.set(newAttrs);
  226. }
  227. });
  228. options.renameCategory("", "Unknown Ontology");
  229. options.sortByProp("category");
  230. options.trigger("update");
  231. },
  232. },
  233. );
  234. });