| // Copyright (c) 2021, Compiler Explorer Authors |
| // All rights reserved. |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are met: |
| // |
| // * Redistributions of source code must retain the above copyright notice, |
| // this list of conditions and the following disclaimer. |
| // * Redistributions in binary form must reproduce the above copyright |
| // notice, this list of conditions and the following disclaimer in the |
| // documentation and/or other materials provided with the distribution. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE |
| // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
| // POSSIBILITY OF SUCH DAMAGE. |
| |
| import $ from 'jquery'; |
| import {options} from '../options.js'; |
| import * as local from '../local.js'; |
| import {Library, LibraryVersion} from '../options.interfaces.js'; |
| import {Lib, WidgetState} from './libs-widget.interfaces.js'; |
| import {unwrapString} from '../assert.js'; |
| |
| const FAV_LIBS_STORE_KEY = 'favlibs'; |
| |
| export type CompilerLibs = Record<string, Library>; |
| type LangLibs = Record<string, CompilerLibs>; |
| type AvailableLibs = Record<string, LangLibs>; |
| type LibInUse = {libId: string; versionId: string} & LibraryVersion; |
| |
| type FavLibraries = Record<string, string[]>; |
| |
| export class LibsWidget { |
| private domRoot: JQuery; |
| |
| private currentLangId: string; |
| private currentCompilerId: string; |
| |
| private dropdownButton: JQuery; |
| private searchResults: JQuery; |
| |
| private readonly onChangeCallback: () => void; |
| |
| private readonly availableLibs: AvailableLibs; |
| |
| constructor( |
| langId: string, |
| compiler: any, |
| dropdownButton: JQuery, |
| state: WidgetState, |
| onChangeCallback: () => void, |
| possibleLibs: CompilerLibs, |
| ) { |
| this.dropdownButton = dropdownButton; |
| if (compiler) { |
| this.currentCompilerId = compiler.id; |
| } else { |
| this.currentCompilerId = '_default_'; |
| } |
| this.currentLangId = langId; |
| this.domRoot = $('#library-selection').clone(true); |
| this.initButtons(); |
| this.onChangeCallback = onChangeCallback; |
| this.availableLibs = {}; |
| this.updateAvailableLibs(possibleLibs); |
| this.loadState(state); |
| |
| this.fullRefresh(); |
| |
| const searchInput = this.domRoot.find('.lib-search-input'); |
| |
| if (window.compilerExplorerOptions.mobileViewer) { |
| this.domRoot.addClass('mobile'); |
| } |
| |
| this.domRoot.on('shown.bs.modal', () => { |
| searchInput.trigger('focus'); |
| }); |
| |
| searchInput.on('input', this.startSearching.bind(this)); |
| |
| this.domRoot.find('.lib-search-button').on('click', this.startSearching.bind(this)); |
| |
| this.dropdownButton.on('click', () => { |
| this.domRoot.modal({}); |
| }); |
| |
| this.updateButton(); |
| } |
| |
| onChange() { |
| this.updateButton(); |
| this.onChangeCallback(); |
| } |
| |
| loadState(state: WidgetState) { |
| for (const lib of state.libs ?? []) { |
| if (lib.name && lib.ver) { |
| this.markLibrary(lib.name, lib.ver, true); |
| } else if (lib.id && lib.version) { |
| this.markLibrary(lib.id, lib.version, true); |
| } |
| } |
| } |
| |
| initButtons() { |
| this.searchResults = this.domRoot.find('.lib-results-items'); |
| } |
| |
| fullRefresh() { |
| this.showSelectedLibs(); |
| this.showSelectedLibsAsSearchResults(); |
| this.showFavorites(); |
| } |
| |
| updateButton() { |
| const selectedLibs = this.get(); |
| let text = 'Libraries'; |
| if (selectedLibs.length > 0) { |
| this.dropdownButton |
| .addClass('btn-success') |
| .removeClass('btn-light') |
| .prop('title', 'Current libraries:\n' + selectedLibs.map(lib => '- ' + lib.name).join('\n')); |
| text += ' (' + selectedLibs.length + ')'; |
| } else { |
| this.dropdownButton.removeClass('btn-success').addClass('btn-light').prop('title', 'Include libs'); |
| } |
| |
| this.dropdownButton.find('.dp-text').text(text); |
| } |
| |
| getFavorites(): FavLibraries { |
| return JSON.parse(local.get(FAV_LIBS_STORE_KEY, '{}')); |
| } |
| |
| setFavorites(faves: FavLibraries) { |
| local.set(FAV_LIBS_STORE_KEY, JSON.stringify(faves)); |
| } |
| |
| isAFavorite(libId: string, versionId: string): boolean { |
| const faves = this.getFavorites(); |
| if (libId in faves) { |
| return faves[libId].includes(versionId); |
| } |
| |
| return false; |
| } |
| |
| addToFavorites(libId: string, versionId: string) { |
| const faves = this.getFavorites(); |
| if (libId in faves) { |
| faves[libId].push(versionId); |
| } else { |
| faves[libId] = []; |
| faves[libId].push(versionId); |
| } |
| |
| this.setFavorites(faves); |
| } |
| |
| removeFromFavorites(libId: string, versionId: string) { |
| const faves = this.getFavorites(); |
| if (libId in faves) { |
| faves[libId] = faves[libId].filter(v => v !== versionId); |
| } |
| |
| this.setFavorites(faves); |
| } |
| |
| newFavoriteLibDiv(libId: string, versionId: string, lib: Library, version: LibraryVersion): JQuery<Node> { |
| const template = $('#lib-favorite-tpl'); |
| |
| const libDiv = $(template.children()[0].cloneNode(true)); |
| |
| const quickSelectButton = libDiv.find('.lib-name-and-version'); |
| quickSelectButton.html(lib.name + ' ' + version.version); |
| quickSelectButton.on('click', () => { |
| this.selectLibAndVersion(libId, versionId); |
| this.showSelectedLibs(); |
| this.onChange(); |
| }); |
| |
| return libDiv; |
| } |
| |
| showFavorites() { |
| const favoritesDiv = this.domRoot.find('.lib-favorites'); |
| favoritesDiv.html(''); |
| |
| const faves = this.getFavorites(); |
| for (const libId in faves) { |
| const versionArr = faves[libId]; |
| for (const versionId of versionArr) { |
| const lib = this.getLibInfoById(libId); |
| if (lib) { |
| if (versionId in lib.versions) { |
| const version = lib.versions[versionId]; |
| const div: any = this.newFavoriteLibDiv(libId, versionId, lib, version); |
| favoritesDiv.append(div); |
| } |
| } |
| } |
| } |
| } |
| |
| clearSearchResults() { |
| this.searchResults.html(''); |
| } |
| |
| newSelectedLibDiv(libId: string, versionId: string, lib: Library, version: LibraryVersion): JQuery<Node> { |
| const template = $('#lib-selected-tpl'); |
| |
| const libDiv = $(template.children()[0].cloneNode(true)); |
| |
| const detailsButton = libDiv.find('.lib-name-and-version'); |
| detailsButton.html(lib.name + ' ' + version.version); |
| detailsButton.on('click', () => { |
| this.clearSearchResults(); |
| this.addSearchResult(libId, lib); |
| }); |
| |
| const deleteButton = libDiv.find('.lib-remove'); |
| deleteButton.on('click', () => { |
| this.markLibrary(libId, versionId, false); |
| libDiv.remove(); |
| this.showSelectedLibs(); |
| this.onChange(); |
| // We need to refresh the library lists, or the selector will still show up with the old library version |
| this.startSearching(); |
| }); |
| |
| return libDiv; |
| } |
| |
| conjureUpExamples(result: JQuery<Node>, lib: Library) { |
| const examples = result.find('.lib-examples'); |
| if (lib.examples && lib.examples.length > 0) { |
| examples.append($('<b>Examples</b>')); |
| const examplesList = $('<ul />'); |
| for (const exampleId of lib.examples) { |
| const li = $('<li />'); |
| examplesList.append(li); |
| const exampleLink = $('<a>Example</a>'); |
| exampleLink.attr('href', `${window.httpRoot}z/${exampleId}`); |
| exampleLink.attr('target', '_blank'); |
| exampleLink.attr('rel', 'noopener'); |
| li.append(exampleLink); |
| } |
| examples.append(examplesList); |
| } |
| } |
| |
| newSearchResult(libId: string, lib: Library): JQuery<Node> { |
| const template = $('#lib-search-result-tpl'); |
| |
| const result = $($(template.children()[0].cloneNode(true))); |
| result.find('.lib-name').html(lib.name || libId); |
| if (!lib.description) { |
| result.find('.lib-description').hide(); |
| } else { |
| result.find('.lib-description').html(lib.description); |
| } |
| result.find('.lib-website-link').attr('href', lib.url ?? '#'); |
| |
| this.conjureUpExamples(result, lib); |
| |
| const faveButton = result.find('.lib-fav-button'); |
| const faveStar = faveButton.find('.lib-fav-btn-icon'); |
| faveButton.hide(); |
| |
| const versions = result.find('.lib-version-select'); |
| versions.html(''); |
| const noVersionSelectedOption = $('<option value="">-</option>'); |
| versions.append(noVersionSelectedOption); |
| let hasVisibleVersions = false; |
| |
| const versionsArr = Object.keys(lib.versions).map(id => { |
| return {id: id, order: lib.versions[id]['$order']}; |
| }); |
| versionsArr.sort((a, b) => b.order - a.order); |
| |
| for (const libVersion of versionsArr) { |
| const versionId = libVersion.id; |
| const version = lib.versions[versionId]; |
| const option = $('<option>'); |
| if (version.used) { |
| option.attr('selected', 'selected'); |
| |
| if (this.isAFavorite(libId, versionId)) { |
| faveStar.removeClass('far').addClass('fas'); |
| } |
| |
| faveButton.show(); |
| } |
| option.attr('value', versionId); |
| option.html(version.version || versionId); |
| if (version.used || !version.hidden) { |
| hasVisibleVersions = true; |
| versions.append(option); |
| } |
| } |
| |
| if (!hasVisibleVersions) { |
| noVersionSelectedOption.text('No available versions'); |
| versions.prop('disabled', true); |
| } |
| |
| faveButton.on('click', () => { |
| const option = versions.find('option:selected'); |
| const verId = option.attr('value') as string; |
| if (this.isAFavorite(libId, verId)) { |
| this.removeFromFavorites(libId, verId); |
| faveStar.removeClass('fas').addClass('far'); |
| } else { |
| this.addToFavorites(libId, verId); |
| faveStar.removeClass('far').addClass('fas'); |
| } |
| this.showFavorites(); |
| }); |
| |
| versions.on('change', () => { |
| const option = versions.find('option:selected'); |
| const verId = option.attr('value') as string; |
| |
| this.selectLibAndVersion(libId, verId); |
| this.showSelectedLibs(); |
| |
| if (this.isAFavorite(libId, verId)) { |
| faveStar.removeClass('far').addClass('fas'); |
| } else { |
| faveStar.removeClass('fas').addClass('far'); |
| } |
| |
| // Is this the "No selection" option? |
| if (verId.length > 0) { |
| faveButton.show(); |
| } else { |
| faveButton.hide(); |
| } |
| |
| this.onChange(); |
| }); |
| |
| return result; |
| } |
| |
| addSearchResult(libId: string, library: Library) { |
| // FIXME: Type mismatch. |
| // The any here stops TS from complaining |
| const result: any = this.newSearchResult(libId, library); |
| this.searchResults.append(result); |
| } |
| |
| static _libVersionMatchesQuery(library: Library, searchText: string): boolean { |
| const text = searchText.toLowerCase(); |
| return ( |
| library.name?.toLowerCase()?.includes(text) || library.description?.toLowerCase()?.includes(text) || false |
| ); |
| } |
| |
| startSearching() { |
| const searchText = unwrapString(this.domRoot.find('.lib-search-input').val()); |
| |
| this.clearSearchResults(); |
| |
| const currentAvailableLibs = this.availableLibs[this.currentLangId][this.currentCompilerId]; |
| if (Object.keys(currentAvailableLibs).length === 0) { |
| const nolibsMessage: any = $($('#libs-dropdown').children()[0].cloneNode(true)); |
| this.searchResults.append(nolibsMessage); |
| return; |
| } |
| |
| for (const libId in currentAvailableLibs) { |
| const library = currentAvailableLibs[libId]; |
| |
| if ('autodetect' in library.versions) continue; |
| |
| if (LibsWidget._libVersionMatchesQuery(library, searchText)) { |
| this.addSearchResult(libId, library); |
| } |
| } |
| } |
| |
| showSelectedLibs() { |
| const items = this.domRoot.find('.libs-selected-items'); |
| items.html(''); |
| |
| const selectedLibs = this.listUsedLibs(); |
| for (const libId in selectedLibs) { |
| const versionId = selectedLibs[libId]; |
| |
| const lib = this.availableLibs[this.currentLangId][this.currentCompilerId][libId]; |
| const version = lib.versions[versionId]; |
| |
| const libDiv: any = this.newSelectedLibDiv(libId, versionId, lib, version); |
| items.append(libDiv); |
| } |
| } |
| |
| showSelectedLibsAsSearchResults() { |
| this.clearSearchResults(); |
| |
| const currentAvailableLibs = this.availableLibs[this.currentLangId][this.currentCompilerId]; |
| if (Object.keys(currentAvailableLibs).length === 0) { |
| const nolibsMessage: any = $($('#libs-dropdown').children()[0].cloneNode(true)); |
| this.searchResults.append(nolibsMessage); |
| return; |
| } |
| |
| for (const libId in currentAvailableLibs) { |
| const library = currentAvailableLibs[libId]; |
| |
| if ('autodetect' in library.versions) continue; |
| |
| const card: any = this.newSearchResult(libId, library); |
| this.searchResults.append(card); |
| } |
| } |
| |
| initLangDefaultLibs() { |
| const defaultLibs = options.defaultLibs[this.currentLangId]; |
| if (!defaultLibs) return; |
| for (const libPair of defaultLibs.split(':')) { |
| const pairSplits = libPair.split('.'); |
| if (pairSplits.length === 2) { |
| const lib = pairSplits[0]; |
| const ver = pairSplits[1]; |
| this.markLibrary(lib, ver, true); |
| } |
| } |
| } |
| |
| updateAvailableLibs(possibleLibs: CompilerLibs) { |
| if (!(this.currentLangId in this.availableLibs)) { |
| this.availableLibs[this.currentLangId] = {}; |
| } |
| |
| if (!(this.currentCompilerId in this.availableLibs[this.currentLangId])) { |
| if (this.currentCompilerId === '_default_') { |
| this.availableLibs[this.currentLangId][this.currentCompilerId] = $.extend( |
| true, |
| {}, |
| options.libs[this.currentLangId], |
| ); |
| } else { |
| this.availableLibs[this.currentLangId][this.currentCompilerId] = $.extend(true, {}, possibleLibs); |
| } |
| } |
| |
| this.initLangDefaultLibs(); |
| } |
| |
| setNewLangId(langId: string, compilerId: string, possibleLibs: CompilerLibs) { |
| const libsInUse = this.listUsedLibs(); |
| |
| this.currentLangId = langId; |
| |
| if (compilerId) { |
| this.currentCompilerId = compilerId; |
| } else { |
| this.currentCompilerId = '_default_'; |
| } |
| |
| // Clear the dom Root so it gets rebuilt with the new language libraries |
| this.updateAvailableLibs(possibleLibs); |
| |
| for (const libId in libsInUse) { |
| this.markLibrary(libId, libsInUse[libId], true); |
| } |
| |
| this.fullRefresh(); |
| this.onChange(); |
| } |
| |
| getVersionOrAlias(name: string, versionId: string): string | null { |
| const lib = this.getLibInfoById(name); |
| if (!lib) return null; |
| // If it's already a key, return it directly |
| if (versionId in lib.versions) { |
| return versionId; |
| } else { |
| // Else, look in each version and see if it has the id as an alias |
| for (const verId in lib.versions) { |
| const version = lib.versions[verId]; |
| if (version.alias.includes(versionId)) { |
| return verId; |
| } |
| } |
| return null; |
| } |
| } |
| |
| getLibInfoById(libId: string): Library | undefined { |
| if ( |
| this.currentLangId in this.availableLibs && |
| this.currentCompilerId in this.availableLibs[this.currentLangId] && |
| libId in this.availableLibs[this.currentLangId][this.currentCompilerId] |
| ) { |
| return this.availableLibs[this.currentLangId][this.currentCompilerId][libId]; |
| } else { |
| return undefined; |
| } |
| } |
| |
| markLibrary(name: string, versionId: string, used: boolean) { |
| const actualId = this.getVersionOrAlias(name, versionId); |
| if (actualId != null) { |
| const v = this.getLibInfoById(name)?.versions[actualId]; |
| if (v != null) { |
| v.used = used; |
| } |
| } |
| } |
| |
| selectLibAndVersion(libId: string, versionId: string) { |
| const actualId = this.getVersionOrAlias(libId, versionId); |
| const libInfo = this.getLibInfoById(libId); |
| for (const v in libInfo?.versions) { |
| // @ts-ignore Sadly the TS type checker is not capable of inferring this can't be null |
| const version = libInfo.versions[v]; |
| version.used = v === actualId; |
| } |
| } |
| |
| get(): Lib[] { |
| const result: Lib[] = []; |
| const usedLibs = this.listUsedLibs(); |
| for (const libId in usedLibs) { |
| result.push({name: libId, ver: usedLibs[libId]}); |
| } |
| return result; |
| } |
| |
| listUsedLibs(): Record<string, string> { |
| const libs: Record<string, string> = {}; |
| const currentAvailableLibs = this.availableLibs[this.currentLangId][this.currentCompilerId]; |
| for (const libId in currentAvailableLibs) { |
| const library = currentAvailableLibs[libId]; |
| for (const verId in library.versions) { |
| if (library.versions[verId].used) { |
| libs[libId] = verId; |
| } |
| } |
| } |
| return libs; |
| } |
| |
| getLibsInUse(): LibInUse[] { |
| const libs: LibInUse[] = []; |
| const currentAvailableLibs = this.availableLibs[this.currentLangId][this.currentCompilerId]; |
| for (const libId in currentAvailableLibs) { |
| const library = currentAvailableLibs[libId]; |
| for (const verId in library.versions) { |
| if (library.versions[verId].used) { |
| libs.push({...library.versions[verId], libId: libId, versionId: verId}); |
| } |
| } |
| } |
| return libs; |
| } |
| } |