mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
Alrighty, round 2! This PR makes the compiler picker dropdown taller (max height is to the bottom of the screen)  I've added a toolbar button for opening the popout as well as a button when the dropdown is open:   Then the modal interface allows text searching, filtering by compiler category, and filtering by compiler instruction set. In the future I'd like to replace the instructionSet property with an architecture property so the filtering here can be a little better.  Demo of the category filtering:  The text filtering highlights results the same as tomselect does:  I moved the favorites permanently to their own column so compilers aren't jarringly shifted around the screen when favoriting / unfavoriting. Also added media queries so the modal looks ok on smaller screens:   I think the only other two changes from the first PR are now the active compiler is highlighted like it is in the tomselect dropdown and I figured out how to reliably focus the search bar on modal open. (Oh, and now clicking a compiler actually does something!) --------- Co-authored-by: Matt Godbolt <matt@godbolt.org>
294 lines
12 KiB
TypeScript
294 lines
12 KiB
TypeScript
// Copyright (c) 2022, 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 TomSelect from 'tom-select';
|
|
|
|
import {ga} from '../analytics.js';
|
|
import * as local from '../local.js';
|
|
import {EventHub} from '../event-hub.js';
|
|
import {Hub} from '../hub.js';
|
|
import {CompilerService} from '../compiler-service.js';
|
|
import {CompilerInfo} from '../../types/compiler.interfaces.js';
|
|
import {unique} from '../../lib/common-utils.js';
|
|
import {unwrap} from '../assert.js';
|
|
import {CompilerPickerPopup} from './compiler-picker-popup.js';
|
|
|
|
type Favourites = {
|
|
[compilerId: string]: boolean;
|
|
};
|
|
|
|
export class CompilerPicker {
|
|
static readonly favoriteGroupName = '__favorites__';
|
|
static readonly favoriteStoreKey = 'favCompilerIds';
|
|
static nextSelectorId = 1;
|
|
domNode: HTMLSelectElement;
|
|
eventHub: EventHub;
|
|
id: number;
|
|
compilerService: CompilerService;
|
|
onCompilerChange: (x: string) => any;
|
|
tomSelect: TomSelect | null;
|
|
lastLangId: string;
|
|
lastCompilerId: string;
|
|
compilerIsVisible: (any) => any; // TODO => bool probably
|
|
popup: CompilerPickerPopup;
|
|
toolbarPopoutButton: JQuery<HTMLElement>;
|
|
popupTooltip: JQuery<HTMLElement>;
|
|
constructor(
|
|
domRoot: JQuery,
|
|
hub: Hub,
|
|
langId: string,
|
|
compilerId: string,
|
|
onCompilerChange: (x: string) => any,
|
|
compilerIsVisible?: (x: any) => any,
|
|
) {
|
|
this.eventHub = hub.createEventHub();
|
|
this.id = CompilerPicker.nextSelectorId++;
|
|
const compilerPicker = domRoot.find('.compiler-picker')[0];
|
|
if (!(compilerPicker instanceof HTMLSelectElement)) {
|
|
throw new Error('.compiler-picker is not an HTMLSelectElement');
|
|
}
|
|
this.toolbarPopoutButton = domRoot.find('.picker-popout-button');
|
|
domRoot.find('.picker-popout-button').on('click', () => {
|
|
unwrap(this.tomSelect).close();
|
|
this.popup.show();
|
|
});
|
|
this.domNode = compilerPicker;
|
|
this.compilerService = hub.compilerService;
|
|
this.onCompilerChange = onCompilerChange;
|
|
this.eventHub.on('compilerFavoriteChange', this.onCompilerFavoriteChange, this);
|
|
this.tomSelect = null;
|
|
if (compilerIsVisible) {
|
|
this.compilerIsVisible = compilerIsVisible;
|
|
} else {
|
|
this.compilerIsVisible = () => true;
|
|
}
|
|
|
|
this.popup = new CompilerPickerPopup(this);
|
|
|
|
this.initialize(langId, compilerId);
|
|
}
|
|
|
|
destroy() {
|
|
this.eventHub.unsubscribe();
|
|
if (this.tomSelect) this.tomSelect.destroy();
|
|
this.tomSelect = null;
|
|
}
|
|
|
|
private initialize(langId: string, compilerId: string) {
|
|
this.lastLangId = langId;
|
|
this.lastCompilerId = compilerId;
|
|
|
|
const groups = this.getGroups(langId);
|
|
const options = this.getOptions(langId, compilerId);
|
|
|
|
this.tomSelect = new TomSelect(this.domNode, {
|
|
sortField: CompilerService.getSelectizerOrder(),
|
|
valueField: 'id',
|
|
labelField: 'name',
|
|
searchField: ['name'],
|
|
placeholder: '🔍 Select a compiler...',
|
|
optgroupField: '$groups',
|
|
optgroups: groups,
|
|
lockOptgroupOrder: true,
|
|
options: options,
|
|
items: compilerId ? [compilerId] : [],
|
|
dropdownParent: 'body',
|
|
closeAfterSelect: true,
|
|
plugins: ['dropdown_input'],
|
|
maxOptions: 1000,
|
|
onChange: val => {
|
|
// TODO(jeremy-rifkin) I don't think this can be undefined.
|
|
// Typing here needs improvement later anyway.
|
|
/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */
|
|
if (val) {
|
|
const compilerId = val as any as string;
|
|
ga.proxy('send', {
|
|
hitType: 'event',
|
|
eventCategory: 'SelectCompiler',
|
|
eventAction: compilerId,
|
|
});
|
|
this.onCompilerChange(compilerId);
|
|
this.lastCompilerId = compilerId;
|
|
}
|
|
},
|
|
duplicates: true,
|
|
render: <any>{
|
|
option: (data, escape) => {
|
|
const isFavoriteGroup = data.$groups.indexOf(CompilerPicker.favoriteGroupName) !== -1;
|
|
const extraClasses = isFavoriteGroup ? 'fas fa-star fav' : 'far fa-star';
|
|
return (
|
|
'<div class="d-flex"><div>' +
|
|
escape(data.name) +
|
|
'</div>' +
|
|
'<div title="Click to mark or unmark as a favorite" class="ml-auto toggle-fav">' +
|
|
'<i class="' +
|
|
extraClasses +
|
|
'"></i>' +
|
|
'</div>' +
|
|
'</div>'
|
|
);
|
|
},
|
|
},
|
|
});
|
|
|
|
this.tomSelect.on('dropdown_open', () => {
|
|
// Prevent overflowing the window
|
|
const dropdown = unwrap(this.tomSelect).dropdown_content;
|
|
dropdown.style.maxHeight = `${window.innerHeight - dropdown.getBoundingClientRect().top - 10}px`;
|
|
|
|
this.popupTooltip = $(`
|
|
<div class="compiler-picker-dropdown-popout-wrapper">
|
|
<div class="compiler-picker-dropdown-popout">
|
|
<i class="fa-solid fa-arrow-up-right-from-square"></i> Pop Out
|
|
</div>
|
|
</div>
|
|
`);
|
|
// I think tomselect is stealing the click event here. Somehow tomselect's global onclick prevents a click
|
|
// here from firing, maybe related to the dropdown closing and this getting removed from the dom. But,
|
|
// mousedown is a decent workaround.
|
|
this.popupTooltip.on('mousedown', () => {
|
|
unwrap(this.tomSelect).close();
|
|
this.popup.show();
|
|
});
|
|
this.popupTooltip.appendTo(this.toolbarPopoutButton);
|
|
});
|
|
|
|
this.tomSelect.on('dropdown_close', () => {
|
|
this.popupTooltip.remove();
|
|
});
|
|
|
|
$(this.tomSelect.dropdown_content).on('click', '.toggle-fav', evt => {
|
|
evt.preventDefault();
|
|
evt.stopPropagation();
|
|
|
|
if (this.tomSelect) {
|
|
let optionElement = evt.currentTarget.closest('.option');
|
|
const clickedGroup = optionElement.parentElement.dataset.group;
|
|
const value = optionElement.dataset.value;
|
|
const data = this.tomSelect.options[value];
|
|
const isAddingNewFavorite = data.$groups.indexOf(CompilerPicker.favoriteGroupName) === -1;
|
|
const elemTop = optionElement.offsetTop;
|
|
|
|
if (isAddingNewFavorite) {
|
|
data.$groups.push(CompilerPicker.favoriteGroupName);
|
|
this.addToFavorites(data.id);
|
|
} else {
|
|
data.$groups.splice(data.$groups.indexOf(CompilerPicker.favoriteGroupName), 1);
|
|
this.removeFromFavorites(data.id);
|
|
}
|
|
|
|
if (clickedGroup !== CompilerPicker.favoriteGroupName) {
|
|
// If the user clicked on an option that wasn't in the top "Favorite" group, then we just added
|
|
// or removed a bunch of controls way up in the list. Find the new element top and adjust the scroll
|
|
// so the element that was just clicked is back under the mouse.
|
|
optionElement = this.tomSelect.getOption(value);
|
|
const previousSmooth = this.tomSelect.dropdown_content.style.scrollBehavior;
|
|
this.tomSelect.dropdown_content.style.scrollBehavior = 'auto';
|
|
this.tomSelect.dropdown_content.scrollTop += optionElement.offsetTop - elemTop;
|
|
this.tomSelect.dropdown_content.style.scrollBehavior = previousSmooth;
|
|
}
|
|
}
|
|
});
|
|
|
|
this.popup.setLang(groups, options, langId);
|
|
}
|
|
|
|
getOptions(langId: string, compilerId: string): (CompilerInfo & {$groups: string[]})[] {
|
|
const favorites = this.getFavorites();
|
|
return Object.values(this.compilerService.getCompilersForLang(langId) ?? {})
|
|
.filter(e => (this.compilerIsVisible(e) && !e.hidden) || e.id === compilerId)
|
|
.map(e => {
|
|
const $groups = [e.group];
|
|
if (favorites[e.id]) $groups.unshift(CompilerPicker.favoriteGroupName);
|
|
return {
|
|
...e,
|
|
$groups,
|
|
};
|
|
});
|
|
}
|
|
|
|
getGroups(langId: string) {
|
|
const optgroups = this.compilerService.getGroupsInUse(langId);
|
|
optgroups.unshift({
|
|
value: CompilerPicker.favoriteGroupName,
|
|
label: 'Favorites',
|
|
});
|
|
return optgroups;
|
|
}
|
|
|
|
update(langId: string, compilerId: string) {
|
|
this.tomSelect?.destroy();
|
|
this.initialize(langId, compilerId);
|
|
}
|
|
|
|
onCompilerFavoriteChange(id: number) {
|
|
if (this.id !== id) {
|
|
// Rebuild the rest of compiler pickers so they can properly show the new fav status
|
|
this.update(this.lastLangId, this.lastCompilerId);
|
|
}
|
|
}
|
|
|
|
getFavorites(): Favourites {
|
|
return JSON.parse(local.get(CompilerPicker.favoriteStoreKey, '{}'));
|
|
}
|
|
|
|
setFavorites(faves: Favourites) {
|
|
local.set(CompilerPicker.favoriteStoreKey, JSON.stringify(faves));
|
|
}
|
|
|
|
isAFavorite(compilerId: string) {
|
|
return compilerId in this.getFavorites();
|
|
}
|
|
|
|
addToFavorites(compilerId: string) {
|
|
const faves = this.getFavorites();
|
|
faves[compilerId] = true;
|
|
this.setFavorites(faves);
|
|
this.updateTomselectOption(compilerId);
|
|
this.eventHub.emit('compilerFavoriteChange', this.id);
|
|
}
|
|
|
|
removeFromFavorites(compilerId: string) {
|
|
const faves = this.getFavorites();
|
|
delete faves[compilerId];
|
|
this.setFavorites(faves);
|
|
this.updateTomselectOption(compilerId);
|
|
this.eventHub.emit('compilerFavoriteChange', this.id);
|
|
}
|
|
|
|
updateTomselectOption(compilerId: string) {
|
|
if (this.tomSelect) {
|
|
this.tomSelect.updateOption(compilerId, this.tomSelect.options[compilerId]);
|
|
this.tomSelect.refreshOptions(false);
|
|
}
|
|
}
|
|
|
|
selectCompiler(compilerId: string) {
|
|
if (this.tomSelect) {
|
|
this.tomSelect.addItem(compilerId);
|
|
}
|
|
}
|
|
}
|