// 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 {Library, LibraryVersion} from '../options.interfaces.js'; import {Lib, WidgetState} from './libs-widget.interfaces.js'; import {unwrapString} from '../assert.js'; import {localStorage} from '../local.js'; import {Alert} from './alert'; const FAV_LIBS_STORE_KEY = 'favlibs'; export type CompilerLibs = Record; type LangLibs = Record; type AvailableLibs = Record; type LibInUse = {libId: string; versionId: string} & LibraryVersion; type FavLibraries = Record; type PopupAlertFilter = (compiler: string, langId: string) => {title: string; content: string} | null; 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; private readonly filters: PopupAlertFilter[] = [ (compilerId, langId) => { if (langId === 'rust' && ['beta', 'nightly'].includes(compilerId)) { return { title: 'Missing library support for rustc nightly/beta', content: 'Compiler Explorer does not yet support libraries for the rustc nightly/beta compilers.' + 'For library support, please use the stable compiler. Please see tracking issue' + '' + 'compiler-explorer/compiler-explorer#3766 for more information.', }; } return null; }, ]; 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, true); 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'); for (const filter of this.filters) { const filterResult = filter(this.currentCompilerId, this.currentLangId); if (filterResult !== null) { const alertSystem = new Alert(); alertSystem.notify(`${filterResult.title}: ${filterResult.content}`, { group: 'libs', alertClass: 'notification-error', }); break; } } }); 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) { // If state exists, clear previously selected libraries. if (state.libs !== undefined) { const libsInUse = this.listUsedLibs(); for (const libId in libsInUse) { this.markLibrary(libId, libsInUse[libId], false); } } 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(localStorage.get(FAV_LIBS_STORE_KEY, '{}')); } setFavorites(faves: FavLibraries) { localStorage.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 { 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 { 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, lib: Library) { const examples = result.find('.lib-examples'); if (lib.examples && lib.examples.length > 0) { examples.append($('Examples')); const examplesList = $('
    '); for (const exampleId of lib.examples) { const li = $('
  • '); examplesList.append(li); const exampleLink = $('Example'); 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 { 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 = $(''); 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 = $('