Files
compiler-explorer/static/widgets/history-widget.ts
Matt Godbolt 637564f389 Migrate to Bootstrap 5 (#7582)
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>
2025-04-24 12:10:37 -05:00

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);
}
}