mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
This PR completes the migration from Bootstrap 4 to Bootstrap 5.3.5 following the plan outlined in [docs/Bootstrap5Migration.md](https://github.com/compiler-explorer/compiler-explorer/blob/mg/bootstrap5/docs/Bootstrap5Migration.md). ## Migration Process We followed a phased approach as documented in the migration plan: 1. **Phase 1: Dependency Updates and Basic Setup** - Updated Bootstrap from 4.6.2 to 5.3.5 - Added @popperjs/core dependency (replacing Popper.js) - Updated Tom Select theme from bootstrap4 to bootstrap5 2. **Phase 2: Global CSS Class Migration** - Updated directional utility classes (ml/mr → ms/me) - Updated floating utility classes (float-left/right → float-start/end) - Updated text alignment classes (text-left/right → text-start/end) 3. **Phase 3: HTML Attribute Updates** - Updated data attributes to use Bootstrap 5 prefixes (data-bs-toggle, data-bs-target, etc.) - Fixed tab navigation issues 4. **Phase 4: JavaScript API Compatibility Layer** - Created bootstrap-utils.ts compatibility layer - Updated component initialization for modals, dropdowns, popovers, etc. 5. **Phase 5: Component Migration** - Updated and tested specific components (modals, dropdowns, toasts, etc.) - Fixed styling issues in cards and button groups 6. **Phase 6: Form System Updates** - Updated form control classes to Bootstrap 5 standards - Updated checkbox/radio markup patterns - Simplified input groups 7. **Phase 7: Navbar Structure Updates** - Updated navbar structure with container-fluid - Fixed responsive behavior 8. **Phase 8: SCSS Variables and Theming** - Added custom CSS fixes for navbar alignment - Verified theme compatibility 9. **Phase 9: Accessibility Improvements** - Updated sr-only to visually-hidden - Added proper ARIA attributes - Enhanced screen reader support ## Key Changes - No more jQuery dependency in Bootstrap 5 - New prefix for data attributes (data-bs-*) - Improved accessibility with ARIA attributes - Updated positioning classes (start/end instead of left/right) - Simplified input group structure ## Test Plan 1. **Navigation Testing** - Verify all dropdown menus open and close properly - Test mobile menu responsiveness - Check tab navigation in settings dialog 2. **Component Testing** - Verify all modals open and close correctly (settings, share, load/save) - Test tooltips and popovers - Check form controls in different dialogs 3. **Layout Testing** - Test responsiveness on different screen sizes - Verify proper alignment of elements - Check dark mode compatibility 4. **Specific Features to Test** - Compiler selection and options - Share dialog functionality - Settings dialog - Tree view (IDE mode) - Font selection dropdown 5. **Browser Testing** - Test in Chrome, Firefox, Safari - Test in mobile browsers ## Note on Further Improvements After this migration is stable, we could consider Phase 12: removing jQuery dependency entirely, as Bootstrap 5 no longer requires it. This would be a separate effort. --------- Co-authored-by: Claude <noreply@anthropic.com>
151 lines
5.7 KiB
TypeScript
151 lines
5.7 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 {editor} from 'monaco-editor';
|
|
import {pluck} from 'underscore';
|
|
import {unwrap} from '../assert.js';
|
|
import * as BootstrapUtils from '../bootstrap-utils.js';
|
|
import {EditorSource, HistoryEntry, sortedList} from '../history.js';
|
|
import ITextModel = editor.ITextModel;
|
|
|
|
type Entry = {dt: number; name: string; load: () => void};
|
|
|
|
export class HistoryWidget {
|
|
private modal: JQuery | null = null;
|
|
private srcDisplay: editor.ICodeEditor | null = null;
|
|
private model: ITextModel = editor.createModel('', 'c++');
|
|
private currentList: HistoryEntry[] = [];
|
|
private onLoadCallback: (data: HistoryEntry) => void = () => {};
|
|
|
|
private initializeIfNeeded() {
|
|
if (this.modal === null) {
|
|
this.modal = $('#history');
|
|
|
|
const placeholder = this.modal.find('.monaco-placeholder');
|
|
this.srcDisplay = editor.create(placeholder[0], {
|
|
fontFamily: 'Consolas, "Liberation Mono", Courier, monospace',
|
|
scrollBeyondLastLine: true,
|
|
readOnly: true,
|
|
minimap: {
|
|
enabled: false,
|
|
},
|
|
});
|
|
|
|
this.srcDisplay.setModel(this.model);
|
|
}
|
|
}
|
|
|
|
private static getLanguagesFromHistoryEntry(entry: HistoryEntry) {
|
|
return pluck(entry.sources, 'lang');
|
|
}
|
|
|
|
private populateFromLocalStorage() {
|
|
this.currentList = sortedList();
|
|
this.populate(
|
|
unwrap(this.modal).find('.historiccode'),
|
|
this.currentList.map((data): Entry => {
|
|
const dt = new Date(data.dt).toString();
|
|
const languages = HistoryWidget.getLanguagesFromHistoryEntry(data).join(', ');
|
|
return {
|
|
dt: data.dt,
|
|
name: `${dt.replace(/\s\(.*\)/, '')} (${languages})`,
|
|
load: () => {
|
|
this.onLoad(data);
|
|
if (this.modal) {
|
|
BootstrapUtils.hideModal(this.modal);
|
|
}
|
|
},
|
|
};
|
|
}),
|
|
);
|
|
}
|
|
|
|
private getContent(src: EditorSource[]) {
|
|
return src.map(val => `/****** ${val.lang} ******/\n${val.source}`).join('\n');
|
|
}
|
|
|
|
private showPreview() {
|
|
const root = unwrap(this.modal).find('.historiccode');
|
|
const elements = root.find('li:not(.template)');
|
|
|
|
for (const elem of elements) {
|
|
const li = $(elem);
|
|
const dt = li.data('dt');
|
|
|
|
const preview = li.find('.preview');
|
|
|
|
if (preview.prop('checked')) {
|
|
const item = this.currentList.find(item => item.dt === dt);
|
|
const text: string = this.getContent(item!.sources);
|
|
this.model.setValue(text);
|
|
|
|
// Syntax-highlight by the language of the 1st source
|
|
let firstLang = HistoryWidget.getLanguagesFromHistoryEntry(item!)[0];
|
|
firstLang = firstLang === 'c++' ? 'cpp' : firstLang;
|
|
editor.setModelLanguage(this.model, firstLang);
|
|
}
|
|
}
|
|
}
|
|
|
|
private populate(root: JQuery, list: Entry[]) {
|
|
root.find('li:not(.template)').remove();
|
|
const template = root.find('.template');
|
|
|
|
for (const elem of list) {
|
|
const li = template.clone().removeClass('template').appendTo(root);
|
|
|
|
li.data('dt', elem.dt);
|
|
|
|
const preview = li.find('.preview');
|
|
|
|
preview.on('click', () => this.showPreview());
|
|
li.find('a').text(elem.name).on('click', elem.load);
|
|
}
|
|
}
|
|
|
|
private resizeLayout() {
|
|
const content = unwrap(this.modal).find('div.src-content');
|
|
this.srcDisplay?.layout({
|
|
width: unwrap(content.width()),
|
|
height: unwrap(content.height()) - 20,
|
|
});
|
|
}
|
|
|
|
private onLoad(data: HistoryEntry) {
|
|
this.onLoadCallback(data);
|
|
}
|
|
|
|
run(onLoad: (data: HistoryEntry) => void) {
|
|
this.initializeIfNeeded();
|
|
this.populateFromLocalStorage();
|
|
this.onLoadCallback = onLoad;
|
|
|
|
// It can't tell that we initialize modal on initializeIfNeeded, so it sticks to the possibility of it being null
|
|
const modalElement = unwrap(this.modal)[0];
|
|
modalElement.addEventListener('shown.bs.modal', () => this.resizeLayout());
|
|
BootstrapUtils.showModal(modalElement);
|
|
}
|
|
}
|