From 637564f3899c03a63734a05a9a62a5745f1b675f Mon Sep 17 00:00:00 2001 From: Matt Godbolt Date: Thu, 24 Apr 2025 12:10:37 -0500 Subject: [PATCH] Migrate to Bootstrap 5 (#7582) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .idea/watcherTasks.xml | 2 +- CLAUDE.md | 62 +++ README.md | 2 + docs/Bootstrap5Migration.md | 387 ++++++++++++++ docs/TestingTheUi.md | 240 +++++++++ .../generate_site_template_screenshots.ts | 2 +- package-lock.json | 25 +- package.json | 4 +- static/bootstrap-utils.ts | 471 ++++++++++++++++++ static/main.ts | 17 +- static/motd.ts | 2 +- static/noscript.ts | 2 +- static/panes/cfg-view.ts | 55 +- static/panes/compiler.ts | 32 +- static/panes/conformance-view.ts | 41 +- static/panes/diff.ts | 5 +- static/panes/editor.ts | 25 +- static/panes/executor.ts | 27 +- static/panes/tree.ts | 7 +- static/real-dark.ts | 2 +- static/sharing.ts | 98 ++-- static/styles/explorer.scss | 116 ++++- static/styles/themes/dark-theme.scss | 33 +- static/styles/themes/default-theme.scss | 15 +- static/styles/themes/one-dark-theme.scss | 35 +- static/styles/themes/pink-theme.scss | 29 +- static/widgets/alert.ts | 41 +- static/widgets/compiler-overrides.ts | 10 +- static/widgets/compiler-picker-popup.ts | 9 +- static/widgets/compiler-picker.ts | 3 +- static/widgets/compiler-version-info.ts | 12 +- static/widgets/fontscale.ts | 6 +- static/widgets/history-widget.ts | 12 +- static/widgets/libs-widget.ts | 46 +- static/widgets/load-save.ts | 31 +- static/widgets/runtime-tools.ts | 6 +- static/widgets/site-templates-widget.ts | 3 +- static/widgets/timing-info-widget.ts | 7 +- views/bits/sponsors-content.pug | 4 +- views/font-size.pug | 2 +- views/index.pug | 207 ++++---- views/menu-policies.pug | 4 +- views/noscript/compiler.pug | 4 +- views/noscript/footer.pug | 2 +- views/noscript/header.pug | 4 +- views/noscript/languages.pug | 2 +- views/popups/alert.pug | 6 +- views/popups/compiler-picker-modal.pug | 4 +- views/popups/enter-something.pug | 13 +- views/popups/history.pug | 6 +- views/popups/library-selection.pug | 9 +- views/popups/load-save.pug | 38 +- views/popups/overrides-selection.pug | 8 +- views/popups/renamepanemodal.pug | 8 +- views/popups/runtimetools-selection.pug | 8 +- views/popups/settings.pug | 92 ++-- views/popups/sharelinkdialog.pug | 29 +- views/popups/site-template-loader.pug | 4 +- views/popups/timing.pug | 6 +- views/popups/yes-no.pug | 8 +- views/templates/panes/cfg.pug | 2 +- views/templates/panes/clangir.pug | 2 +- views/templates/panes/codeEditor.pug | 8 +- views/templates/panes/compiler-output.pug | 2 +- views/templates/panes/compiler.pug | 57 +-- views/templates/panes/device.pug | 2 +- views/templates/panes/diff.pug | 20 +- views/templates/panes/executor.pug | 49 +- views/templates/panes/gccdump.pug | 4 +- views/templates/panes/ir.pug | 4 +- views/templates/panes/opt-pipeline.pug | 9 +- views/templates/panes/opt-view.pug | 2 +- views/templates/panes/tree.pug | 12 +- views/templates/widgets/compiler-selector.pug | 27 +- .../widgets/lib-search-result-tpl.pug | 2 +- views/templates/widgets/libs-entry.pug | 5 +- .../widgets/possible-override-tpl.pug | 2 +- .../widgets/possible-runtime-tool-tpl.pug | 2 +- views/templates/widgets/tree-editor-tpl.pug | 2 +- 79 files changed, 1972 insertions(+), 631 deletions(-) create mode 100644 CLAUDE.md create mode 100644 docs/Bootstrap5Migration.md create mode 100644 docs/TestingTheUi.md create mode 100644 static/bootstrap-utils.ts diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml index 0ad30c1ef..59847fbeb 100644 --- a/.idea/watcherTasks.xml +++ b/.idea/watcherTasks.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..32bd30b59 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,62 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build & Test Commands +- Build: `npm run webpack`, `npm start` +- Dev Mode: `make dev`, `make gpu-dev` +- Lint: `npm run lint` (auto-fix), `npm run lint-check` (check only) +- Type Check: `npm run ts-check` +- Test: `npm run test` (all), `npm run test-min` (minimal) +- Test Single File: `npm run test -- --run base-compiler-tests.ts` +- Test Specific Pattern: `npm run test -- -t "should handle execution failures"` +- Cypress Tests: `npm run cypress` +- Pre-commit Check: `make pre-commit` or `npm run check` + +## Important Workflow Requirements +- ALWAYS run `npm run lint` before any git operations (`git add`, `git commit`, etc.) +- The linter will automatically fix formatting issues, so this must be run before committing +- Failing to run the linter may result in style issues and commit failures + +## Style Guidelines +- TypeScript: Strict typing, no implicit any, no unused locals +- Formatting: 4-space indentation, 120 char line width, single quotes +- No semicolon omission, prefer const/let over var +- Client-side: TypeScript transpiled to ES5 JavaScript. This process requires import of `blah.js` even though `blah.ts` is the actual filename +- ALWAYS place imports at the top of files, never inside functions or methods, unless absolutely necessary (and confirm before proposing) +- Use Underscore.js for utility functions +- Write tests for new server-side components +- Where appropriate suggest follow-up improvements to code to improve code quality, and DRY up where feasible +- Documentation is in `docs/` directory; update where necessary, in particular if anything about the RESTful API changes +- Don't add comments above code that's clearly self-documenting. For example, don't put comments like this: + ``` + // Initialises the thing + initialiseThing(); + ``` + +## Testing Guidelines +- Use Vitest for unit tests (compatible with Jest syntax) +- Tests are in the `/test` directory, typically named like the source files they test +- Use spy functions with `vi.spyOn()` for mocking dependencies +- Test structure follows describe/it pattern with descriptive test names +- Separate tests with clear section headers using comments for readability +- Consider cross-platform compatibility (especially Windows path handling) +- For complex files, organize tests by functionality rather than by method +- Use `beforeEach`/`afterEach` to set up and clean up test environment +- Remember to restore mocks with `vi.restoreAllMocks()` after tests +- Test both success and error cases +- Coverage is available with `npm test:coverage` +- For Windows-specific path issues, either: + - Skip tests with `if (process.platform === 'win32') return;` + - Write platform-specific assertions + - Use path-agnostic checks + +## Compiler Testing Specifics +- Mock filesystem operations when testing file I/O +- Use `makeFakeCompilerInfo()` for creating test compiler configurations +- Use `makeCompilationEnvironment()` to create test environments +- Mock `exec` calls for testing compilation and execution +- For BaseCompiler, use the test utils from `/test/utils.js` +- Test specific combinations of compiler capabilities +- Focus tests on behavior, not implementation details +- Use platform-agnostic assertions where possible \ No newline at end of file diff --git a/README.md b/README.md index 110d3dc55..dcfd5ef99 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,8 @@ and shorter startup times. You can also use `npm run dev` to run if `make dev` doesn't work on your machine. +When making UI changes, we recommend following the [UI Testing Checklist](docs/TestingTheUi.md) to ensure all components work correctly. + Some languages need extra tools to demangle them, e.g. `rust`, `d`, or `haskell`. Such tools are kept separately in the [tools repo](https://github.com/compiler-explorer/compiler-explorer-tools). diff --git a/docs/Bootstrap5Migration.md b/docs/Bootstrap5Migration.md new file mode 100644 index 000000000..0f9ebf39b --- /dev/null +++ b/docs/Bootstrap5Migration.md @@ -0,0 +1,387 @@ +# Bootstrap 5 Migration Plan + +This document outlines the step-by-step process for migrating Compiler Explorer from Bootstrap 4 to Bootstrap 5.3.5. The +migration will be completed incrementally to allow for testing between steps. + +## Migration Strategy + +We'll break down the migration into smaller, testable chunks rather than making all changes at once. This approach +allows for: + +- Easier identification of issues +- Progressive testing by project maintainers +- Minimizing disruption to the codebase + +**Progress tracking will be maintained in this document.** + +## Phases & Current Progress + +### Phase 1: Dependency Updates and Basic Setup ✅ + +- [x] Update package.json with Bootstrap 5.3.5 +- [x] Add @popperjs/core dependency (replacing Popper.js) +- [x] Update Tom Select theme from bootstrap4 to bootstrap5 +- [x] Update main import statements where Bootstrap is initialized +- [x] Update webpack configuration if needed for Bootstrap 5 compatibility +- [x] Verify the application still builds and runs with basic functionality + +#### Notes for Human Testers (Phase 1) + +- At this phase, the application will have console errors related to Bootstrap component initialization +- Tom Select dropdowns (like compiler picker) may have styling differences with the new bootstrap5 theme +- Initial page load should still work, but dropdown functionality will be broken +- Modal dialogs like Settings and Sharing may not function correctly +- The primary layout and basic display of panes should still function + +### Phase 2: Global CSS Class Migration ✅ + +- [x] Update directional utility classes (ml/mr → ms/me) + - [x] Search and replace in .pug templates + - [x] Search and replace in .scss files + - [x] Search and replace in JavaScript/TypeScript files that generate HTML +- [x] Update floating utility classes (float-left/right → float-start/end) +- [x] Update text alignment classes (text-left/right → text-start/end) +- [x] Update other renamed classes (badge-pill → rounded-pill, etc.) +- [x] Test and verify styling changes + +#### Notes for Human Testers (Phase 2) + +- Look for proper spacing and margin alignment in the UI +- Specific components to check: + - Compiler output area: Check the spacing in compiler.pug (short-compiler-name, compile-info spans) + - Navbar spacing: The navbar items should maintain proper spacing (ms/me instead of ml/mr) + - Code editor components: Check the button and icon alignment in codeEditor.pug + - Tree view components: The tree.pug file had utility class changes + - Alert messages (widgets/alert.ts): Check that toast messages appear with correct alignment + - Compiler picker (compiler-picker.ts and compiler-picker-popup.ts): Check dropdown spacing + - Rounded badge display in menu-policies.pug (now using rounded-pill instead of badge-pill) +- The float-end class replaces float-right in the index.pug file's copy buttons +- Any LTR/RTL layout impacts should be especially checked for correct directionality + +### Phase 3: HTML Attribute Updates ✅ + +- [x] Update data attributes across the codebase + - [x] data-toggle → data-bs-toggle + - [x] data-target → data-bs-target + - [x] data-dismiss → data-bs-dismiss + - [x] data-ride → data-bs-ride (not used in codebase) + - [x] data-spy → data-bs-spy (not used in codebase) + - [x] Other data attributes as needed +- [x] Test components to ensure they function correctly with new attributes + +#### Notes for Human Testers (Phase 3) + +- This phase should restore basic functionality of several Bootstrap components +- Specific components to check: + - Dropdowns: All dropdown menus throughout the application should open/close properly + - Modal dialogs: Settings, Sharing, and other modal dialogs should open/close correctly + - Tooltips: Hover tooltips (using data-bs-toggle="tooltip") should display properly + - Popovers: Any popovers used in the UI should function correctly + - Collapse components: Any collapsible sections should toggle properly + - Tabs: Any tabbed interfaces should switch between tabs correctly +- Watch for console errors related to Bootstrap component initialization +- Some JavaScript component initialization may still be broken until Phase 4 is completed + +### Phase 4: JavaScript API Compatibility Layer ✅ + +- [x] Create a temporary Bootstrap compatibility utility module to abstract component initialization + - [x] Implement a hybrid approach with `bootstrap-utils.ts` as a compatibility layer + - [x] Mark clearly as temporary code to be removed after migration is complete + - [x] Define methods for each component type (Modal, Dropdown, Toast, etc.) +- [x] Update component initialization in key files: + - [x] widgets/alert.ts (modals and toasts) + - [x] sharing.ts (modals, tooltips, and dropdowns) + - [x] compiler-picker-popup.ts (modals) + - [x] load-save.ts (modals) + - [x] Other files with Bootstrap component initialization: + - [x] **Modal Initialization**: + - [x] widgets/site-templates-widget.ts + - [x] widgets/runtime-tools.ts + - [x] widgets/compiler-overrides.ts + - [x] widgets/timing-info-widget.ts + - [x] widgets/history-widget.ts + - [x] widgets/libs-widget.ts + - [x] main.ts + - [x] **Dropdown Handling**: + - [x] panes/tree.ts + - [x] panes/compiler.ts + - [x] panes/editor.ts + - [x] **Popover Handling**: + - [x] main.ts + - [x] widgets/compiler-version-info.ts + - [x] panes/executor.ts + - [x] panes/conformance-view.ts + - [x] panes/cfg-view.ts +- [x] Test the compatibility layer with basic components + +#### Notes for Human Testers (Phase 4) + +- This is one of the most critical phases as it involves creating a compatibility layer for the JavaScript API +- A new utility file will be created for component initialization that abstracts Bootstrap 5's new approach +- Key differences to watch for: + - Modal initialization and events: Bootstrap 5 uses a completely different event system + - Dropdown initialization: Works with data attributes but requires `data-bs-toggle` instead of `data-toggle` + - Toast components: The API has changed significantly + - Popover/Tooltip initialization: API changes but still support data attributes with proper prefixes +- Components to thoroughly test: + - Alert dialogs (widgets/alert.ts): Check all types of alerts (info, warning, error) + - Sharing functionality (sharing.ts): The share modal should work properly + - Compiler picker popup (compiler-picker-popup.ts): Should display and function correctly + - All dropdown menus: Should open and close properly + - All tooltips and popovers: Should display correctly on hover/click +- Watch for event handling issues where Bootstrap 4 events no longer exist or are renamed + +### Phase 5: Component Migration (By Component Type) ✅ + +#### Modal Component Migration + +- [x] Update modal implementation in alert.ts +- [x] Update modal usage in compiler-picker-popup.ts +- [x] Update modal handling in load-save.ts +- [x] Update modal event handling in sharing.ts +- [x] Test modal functionality thoroughly + +#### Dropdown Component Migration + +- [x] Update dropdown handling in sharing.ts +- [x] Update dropdown usage in compiler.ts, editor.ts, etc. +- [x] Test dropdown functionality thoroughly + +#### Toast/Alert Component Migration + +- [x] Update toast implementation in alert.ts +- [x] Update toast styling in explorer.scss +- [x] Test toast notifications and alerts + +#### Popover/Tooltip Migration + +- [x] Update tooltip initialization in sharing.ts +- [x] Update popover usage in compiler.ts, executor.ts, editor.ts, etc. +- [x] Test popover and tooltip functionality thoroughly + +#### Card Component Updates + +- [x] Review card usage and update to Bootstrap 5 standards +- [x] ~~Replace any card-deck implementations with grid system~~ (Not needed - card-deck not used in codebase) +- [x] Test card layouts, especially tab navigation within cards + +#### Collapse Component Updates + +- [x] ~~Update any collapse component implementations~~ (Not needed - minimal collapse usage in codebase) +- [x] Test collapse functionality (limited to navbar hamburger menu on mobile) + +#### Button Group Updates + +- [x] Review button group implementations +- [x] Update to Bootstrap 5 standards (no changes needed - Bootstrap 5 maintains same button group classes) +- [x] Test button group functionality in toolbars and dropdown menus + +### Phase 6: Form System Updates ✅ + +- [x] Update form control classes to Bootstrap 5 standards +- [x] Update input group markup and classes +- [x] Update checkbox/radio markup to Bootstrap 5 standards +- [x] Update form validation classes and markup +- [x] ~~Consider implementing floating labels where appropriate (new in Bootstrap 5)~~ (Not needed for existing form + usage) +- [x] Test form functionality and appearance + +### Phase 7: Navbar Structure Updates ✅ + +- [x] Update navbar structure in templates to match Bootstrap 5 requirements +- [x] Review custom navbar styling in explorer.scss +- [x] Test responsive behavior of navbar +- [x] Ensure mobile menu functionality works correctly +- [x] ~~Consider implementing offcanvas for mobile navigation (new in Bootstrap 5)~~ (Standard navbar collapse is + sufficient for current needs) + +### Phase 8: SCSS Variables and Theming ✅ + +- [x] Review any custom SCSS that extends Bootstrap functionality +- [x] Update any custom themes to use Bootstrap 5 variables +- [x] Check z-index variable changes in Bootstrap 5 +- [x] Add navbar container padding fix for proper alignment +- [x] Test theme switching functionality + +### Phase 9: Accessibility Improvements ✅ + +- [x] Review ARIA attributes in custom component implementations +- [x] Leverage Bootstrap 5's improved accessibility features +- [x] Add ARIA labels and live regions for dynamic content +- [x] Enhance form controls with proper accessibility attributes +- [ ] ~~Test with screen readers and keyboard navigation~~ (left for future work) +- [ ] ~~Ensure color contrast meets accessibility guidelines~~ (left for future work) + +### Phase 10: Final Testing and Refinement ✅ + +- [x] ~~Comprehensive testing across different viewports~~ cursory testing with a few viewports +- [x] Cross-browser testing (at least; looked in FireFox and we're good) +- [x] Fix any styling issues or inconsistencies +- [x] ~~Performance testing (Bootstrap 5 should be more performant)~~ (don't care; site is fine) +- [x] Ensure no regressions in functionality + +## Key Learnings From Implementation + +These insights were gathered during the migration process and may be helpful for future reference: + +- **Data Attributes Still Work Without JavaScript Initialization**: Despite some documentation suggesting otherwise, + Bootstrap 5 components with data attributes (like tabs) still work without explicit JavaScript initialization. The key + is using the correct `data-bs-*` prefix. +- **Close Button Implementation Completely Changed**: Bootstrap 4 used `.close` class with a `×` entity inside a + span, while Bootstrap 5 uses `.btn-close` class with a background image and no inner content. +- **Tab Navigation Issues**: The tab navigation problems were fixed by simply updating data attributes, not by adding + JavaScript initialization. +- **jQuery Plugin Methods Removal**: jQuery methods like `.popover()` and `.dropdown('toggle')` need to be replaced with + code that uses the Bootstrap 5 API through a compatibility layer. Always use `BootstrapUtils` helper methods rather + than direct jQuery plugin calls. +- **Grid and Form Class Renaming**: Bootstrap 5 renamed several core classes, such as changing `.form-row` to `.row`. + This can cause subtle template selector issues in code that relies on these class names. +- **Don't Mix Data Attributes and JavaScript Modal Creation**: When creating modals via JavaScript (e.g., for + dynamically loaded content), don't include `data-bs-toggle="modal"` on the trigger element unless you also add a + matching `data-bs-target` attribute pointing to a valid modal element. +- **Modal Events Changed Significantly**: Bootstrap 5 modal events need to be attached directly to the native DOM + element rather than jQuery objects, and the event parameter type is different. For proper typing, import the `Modal` + type from bootstrap and use `Modal.Event` type. +- **jQuery Event Binding vs Native DOM Events**: Bootstrap 5 requires native DOM event binding instead of jQuery's + `.on()` method. Replace `$(selector).on('shown.bs.modal', handler)` with + `domElement.addEventListener('shown.bs.modal', handler)`. This is particularly important for modal events like ' + shown.bs.modal'. +- **Tooltip API Changed**: The global `window.bootstrap.Tooltip` reference no longer exists. Import the `Tooltip` class + directly from bootstrap instead. +- **Input Group Structure Simplified**: Bootstrap 5 removed the need for `.input-group-prepend` and + `.input-group-append` wrapper divs. Buttons and other controls can now be direct children of the `.input-group` + container. This simplifies the markup but requires template updates. +- **TomSelect Widget Integration**: Bootstrap 5's switch from CSS triangles to SVG background images for dropdowns + caused issues with TomSelect. Adding back custom CSS for dropdown arrows was necessary to maintain correct appearance. +- **Btn-block Removed**: Bootstrap 5 removed the `.btn-block` class. Instead, the recommended approach is to wrap + buttons in a container with `.d-grid` and use standard `.btn` classes. This affects any full-width buttons in the + application. +- **Element Selection for Components**: When working with Bootstrap 5 components, prefer passing CSS selectors to + `BootstrapUtils` methods rather than jQuery objects, as this provides more consistent behavior. + +## Fixed Issues & Completed Work + +### UI Layout & Display Issues + +- [x] Font dropdown styling fixed +- [x] Templates view proportions fixed (min-width added to columns and modal) +- [x] Dialog appearance fixed (updated close buttons to use `.btn-close`) +- [x] Dropdown positioning fixed (updated to `.dropdown-menu-end`) +- [x] TomSelect dropdown arrows fixed (custom CSS implementation) +- [x] IDE mode border styling improved (temporarily with `.list-group-flush`) +- [x] Sponsors window styling fixed (replaced `.btn-block` with `.d-grid` approach) +- [x] The X to close the community/alert notes is harder to see in dark mode than before (fixed: added + `filter: invert(100%)` to make btn-close buttons visible in dark themes) +- [x] TomSelect dropdowns for compilers are excessively long (both in executor view and normal view) (fixed manually) +- [x] Default text/placeholder text is too dark, making it hard to read (especially "Compiler options") (fixed manually) +- [x] Dropdown in the library menu has changed color (fixed: updated `.custom-select` to `.form-select` in theme files) +- [x] ~~Layout has changed slightly in the library menu~~ (decided it looks better now) +- [x] The "popout" on the TomSelect compiler dropdown is misaligned (fixed: updated styling for TomSelect components) +- [x] Compiler combobox rounding overlaps left border by 1 pixel (fixed: overrode CSS variables to reset Bootstrap 5's + negative margin) +- [x] Diff view - changing left/right side compiler/window turns combobox to a white background (fixed: removed + form-select class to avoid transparent background) +- [x] The popular arguments dropdown at the right of the options isn't properly aligned (fixed: updated dropdown styling + in compiler.pug) +- [x] Long compiler names wrap instead of widening the dropdown (fixed: improved styling for TomSelect dropdowns) + +### Navigation & Functional Issues + +- [x] Tab navigation fixed (updated data attributes to `data-bs-toggle="tab"`) +- [x] Share dialog functionality fixed (proper Bootstrap 5 modal initialization) +- [x] Sponsors modal error fixed (removed conflicting data attributes) +- [x] Share dropdown tooltip conflict fixed (moved tooltip to parent element) +- [x] History view is broken (empty when clicking radio buttons) (fixed: updated modal event binding from jQuery's + `.on('shown.bs.modal')` to native DOM `addEventListener('shown.bs.modal')`) +- [x] Conformance view's "add compiler" functionality is broken (fixed: template selector was looking for `.form-row` + which changed to `.row` in Bootstrap 5) +- [x] Need to check for more instances of old Bootstrap v4 code patterns (fixed: replaced `dropdown('toggle')` in + main.ts with `BootstrapUtils.getDropdownInstance()` and `.toggle()`) +- [x] Runtime tools window is broken - doesn't save settings anymore (fixed: updated modal hide event handling with + setElementEventHandler) +- [x] Emulation functionality is broken due to modal issues (fixed: replaced direct .modal() calls with + BootstrapUtils.showModal) + +### Code Structure Improvements + +- [x] Custom classes in runtime tools selection (`.custom-runtimetool`) and overrides selection (`.custom-override`) - + removed as they were superfluous +- [x] `.form-row` still used in theme files (dark-theme.scss, one-dark-theme.scss, pink-theme.scss) - replaced with + standard `.row` +- [x] Border directional properties in explorer.scss updated for better RTL support - added `border-inline-start` and + border radius logical properties with appropriate fallbacks for older browsers +- [x] Input group structures verified - all instances of the deprecated `.input-group-prepend` and `.input-group-append` + have already been updated to use Bootstrap 5's simplified approach +- [x] Toast header close button styling verified - explorer.scss already uses `.btn-close` consistently for toast + components +- [x] Event handlers verified - history-widget.ts and sharing.ts are correctly using native DOM addEventListener methods + with the appropriate Bootstrap 5 event names + +## Future Work + +### Phase 11: Documentation Update + +- [ ] Update any documentation that references Bootstrap components +- [ ] Document custom component implementations +- [ ] Note any deprecated features or changes in functionality + +### Phase 12: Optional jQuery Removal and Cleanup + +- [ ] Create a plan for jQuery removal (if desired) +- [ ] Identify non-Bootstrap jQuery usage that would need refactoring +- [ ] Remove the temporary `bootstrap-utils.ts` compatibility layer + - [ ] Replace all uses with direct Bootstrap 5 API calls + - [ ] Document the native Bootstrap 5 API for future reference +- [ ] Investigate and fix modal accessibility warnings + - [ ] Address the warning: "Blocked aria-hidden on an element because its descendant retained focus" + - [ ] Update modal template markup to leverage Bootstrap 5.3's built-in support for the `inert` attribute + - [ ] Ensure proper focus management in modals for improved accessibility + +### Additional Pending Issues + +- [ ] Check Sentry for additional errors on the beta site +- [ ] Investigate the "focus" selected check boxes in the settings view. They're very light when focused, in particular + in pink theme. I couldn't work out how to fix this, but it seemed minor. +- [ ] The "pop out" div that's attached to the compiler picker doesn't work on the conformance view: this was broken + before. Essentially the z-order means it's drawn behind the lower conformance compilers and `z-index` can't fix it. + Needs a rethink of how this is done. +- [ ] File tracking issues for anything on this list we don't complete. + +## Final Testing Checklist + +Before considering the Bootstrap 5 migration complete, a comprehensive UI testing checklist was created and used to +verify functionality. This checklist has been completed with all tests passing. The tests cover all major UI components +that could be affected by the Bootstrap migration. + +The checklist included: + +- Modal dialogs (Settings, Share, Load/Save, etc.) +- Dropdown components (navigation, compiler options, TomSelect) +- Toast/Alert components +- Popovers and tooltips +- Card, button group, and form components +- Specialized views (Conformance, Tree, Visualization) +- Responsive behavior + +A permanent version of this UI testing checklist has been created as a separate document and can be used for testing +future UI changes or upgrades: [UI Testing Checklist](TestingTheUi.md) + +## Notes for Implementation + +1. **Make minimal changes** in each step to allow for easier testing and troubleshooting +2. **Test thoroughly** after each phase before moving to the next +3. **Document issues** encountered during migration for future reference +4. **Focus on accessibility** to ensure the site remains accessible throughout changes +5. **Maintain browser compatibility** with all currently supported browsers +6. **Consider performance implications** of the changes +7. **NEVER mark any issue as fixed in this document** until you have explicit confirmation from the reviewer that the + issue is completely resolved +8. **NEVER commit changes** until you have explicit confirmation that the fix works correctly + +## Technical References + +- [Bootstrap 5 Migration Guide](https://getbootstrap.com/docs/5.0/migration/) +- [Bootstrap 5 Components Documentation](https://getbootstrap.com/docs/5.3/components/) +- [Bootstrap 5 Utilities Documentation](https://getbootstrap.com/docs/5.3/utilities/) +- [Bootstrap 5 Forms Documentation](https://getbootstrap.com/docs/5.3/forms/overview/) +- [Popper v2 Documentation](https://popper.js.org/docs/v2/) diff --git a/docs/TestingTheUi.md b/docs/TestingTheUi.md new file mode 100644 index 000000000..a8a20db7a --- /dev/null +++ b/docs/TestingTheUi.md @@ -0,0 +1,240 @@ +# UI Testing Checklist for Compiler Explorer + +This document provides a checklist for testing the Compiler Explorer UI components. Use this checklist for +major UI changes, framework updates, or when implementing significant new features. + +## Modal Components + +### Settings Modal + +- Open and close using the "Settings" option in the "More" dropdown +- Test tab navigation between all tab sections (Colouring, Site behaviour, etc.) +- Verify form controls within settings (checkboxes, selects, inputs) +- Check that the close button works +- Verify proper modal appearance/styling in both light and dark themes + +### Share Modal + +- Open and close using the "Share" button +- Verify the URL is generated correctly +- Test the copy button +- Check that social sharing buttons display correctly +- Verify proper styling in both light and dark themes +- Test "Copied to clipboard" tooltip functionality + +### Load/Save Modal + +- Open and close using "Save" or "Load" options +- Test tab navigation between sections (Examples, Browser-local storage, etc.) +- Verify save functionality to browser storage +- Test loading from browser storage +- Check proper styling/layout in both light and dark themes + +### Compiler Picker Modal + +- Open using the popout button next to a compiler selector +- Test filter functionality (architecture, compiler type, search) +- Verify compiler selection works +- Check proper styling in both light and dark themes + +### Other Modals + +- Test confirmation dialogs (alert.ts) +- Test library selection modal +- Test compiler overrides modal +- Test runtime tools modal +- Test templates modal +- Verify proper styling in both light and dark themes + +## Dropdown Components + +### Main Navigation Dropdowns + +- Test "More" dropdown menu (all items work and have proper styling) +- Test "Other" dropdown menu (all items work and have proper styling) +- Verify dropdowns are properly positioned (not clipped) +- Test on different screen sizes to ensure responsive behavior + +### Compiler Option Dropdowns + +- Test filter dropdowns in compiler pane +- Test "Add new..." dropdown +- Test "Add tool..." dropdown +- Test popular arguments dropdown +- Verify proper positioning, especially for dropdowns at the right edge of the screen + +### Editor Dropdowns + +- Test language selector dropdown +- Test font size dropdown +- Verify proper styling and positioning + +### TomSelect Dropdowns + +- Test compiler selectors +- Test library version selectors +- Verify dropdown arrows appear correctly +- Verify dropdown items are styled correctly + +## Toast/Alert Components + +### Alert Notifications + +- Trigger various notifications (info, warning, error) +- Verify proper styling +- Test auto-dismiss functionality +- Check that close button works +- Test stacking behavior of multiple notifications + +### Alert Dialogs + +- Test info/warning/error alert dialogs (using the Alert class) +- Verify proper styling and positioning +- Check button functionality within dialogs + +## Popover/Tooltip Components + +### Tooltips + +- Hover over various buttons with tooltips (toolbar buttons, share button, etc.) +- Verify tooltip text appears correctly +- Check tooltip positioning (above/below/left/right of trigger) +- Verify proper styling in both light and dark themes + +### Popovers + +- Trigger popovers on compiler info +- Check popover content displays correctly +- Verify popover positioning +- Test dismissal by clicking outside +- Verify proper styling in both light and dark themes + +## Card Components + +- Check card styling in modals (Settings, Load/Save, etc.) +- Verify tab navigation within card headers +- Test card body content layout +- Check responsive behavior on different screen sizes + +## Button Group Components + +### Toolbar Button Groups + +- Test button groups in compiler pane toolbar +- Test button groups in editor pane toolbar +- Verify proper alignment and styling +- Check dropdown buttons within button groups + +### Other Button Groups + +- Test font size button group +- Test bottom bar button groups +- Verify proper styling in both light and dark themes + +## Collapse Components + +- Test mobile view hamburger menu +- Verify menu expands/collapses correctly +- Check that all menu items are accessible in collapsed mode + +## Specialized Views + +### Conformance View + +- Test compiler selectors and options +- Verify results display correctly +- Test the "add compiler" functionality +- Verify that compilers can be added and removed +- Check that conformance testing works end-to-end + +### Tree View (IDE Mode) + +- Check tree structure and file display +- Test right-click menus and dropdowns +- Verify file manipulation controls + +### Visualization Components + +- Test CFG view rendering and controls +- Check opt pipeline viewer +- Verify AST view + +### Sponsor Window + +- Check sponsor list display +- Verify modal dialog appearance and functionality + +## Form Components + +- Verify form control styling (inputs, selects, checkboxes) +- Test input groups with buttons +- Check validation states + +## Responsive Behavior + +- Test at various viewport sizes +- Verify mobile menu functionality +- Check input group stacking behavior + +## Runtime Tool Integration + +### Runtime Tools + +- Open the runtime tools window from compiler pane +- Change settings and click outside the modal to close +- Verify settings are properly saved +- Test with multiple runtime tool options +- Verify event handling properly handles modal opening/closing + +### Emulation Features + +- Test BBC emulation by clicking emulator links +- Check Z80 emulation features (e.g. https://godbolt.org/z/qnE7jhnvc) +- Verify emulator modals open properly +- Test interaction between emulator windows and the main interface + +## Diff View + +- Test changing compilers in both left and right panes +- Verify backgrounds remain themed correctly in dark mode +- Check that the diff view layout is correct (no excessive height) +- Confirm that input groups and buttons are properly sized +- Test different diff view types (Assembly, Compiler output, etc.) + +## TomSelect and Input Components + +### Compiler Selection Dropdowns + +- Verify long compiler names display properly without excessive wrapping +- Check that dropdowns expand to fit compiler names rather than wrapping text +- Test the flex-grow behavior of dropdown elements +- Check the alignment of the popout button on all dropdowns +- Verify border colors in dark themes are appropriate + +### Placeholder Text + +- Check visibility and contrast of placeholder text in all input fields +- Specifically test "Compiler options" field visibility +- Verify that all placeholder text is readable in both light and dark themes + +## Library Components + +### Library Menu + +- Check dropdown colors and layout +- Verify all library functionality works correctly +- Test adding and removing libraries +- Check library version selection + +## History View + +- Verify the history view populates correctly when clicking radio buttons +- Test all history functions (load, delete, etc.) + +## General Testing + +- Check pixel-perfect alignment of elements (compare with live site) +- Test all components for unexpected behavioral differences +- Verify theme switching works correctly for all components +- Test cross-browser compatibility (at least Firefox and Chrome) +- Check accessibility features (tab navigation, screen reader support) diff --git a/etc/scripts/generate_site_template_screenshots.ts b/etc/scripts/generate_site_template_screenshots.ts index 96f6c0ba2..b607d457b 100644 --- a/etc/scripts/generate_site_template_screenshots.ts +++ b/etc/scripts/generate_site_template_screenshots.ts @@ -92,7 +92,7 @@ async function generateScreenshot(url: string, output_path: string, settings, wi }, settings); await page.goto(url); //await sleep(2000); - //await page.click(".modal.show button.btn.btn-outline-primary[data-dismiss=modal]"); + //await page.click(".modal.show button.btn.btn-outline-primary[data-bs-dismiss=modal]"); //await sleep(5000); //await page.click("#simplecook .btn.btn-primary.btn-sm.cook-do-consent"); await page.evaluate(() => { diff --git a/package-lock.json b/package-lock.json index d424c4838..423bb1b8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,11 +18,12 @@ "@flatten-js/interval-tree": "^1.1.3", "@fortawesome/fontawesome-free": "^6.7.2", "@orchidjs/sifter": "^1.1.0", + "@popperjs/core": "^2.11.8", "@sentry/browser": "^7.120.3", "@sentry/node": "^7.120.3", "@types/semver": "^7.7.0", "big-integer": "^1.6.52", - "bootstrap": "^4.6.2", + "bootstrap": "^5.3.5", "buffer": "^6.0.3", "chart.js": "^4.4.9", "clipboard": "^2.0.11", @@ -48,7 +49,6 @@ "nopt": "^8.1.0", "p-queue": "^8.1.0", "path-browserify": "^1.0.1", - "popper.js": "^1.16.1", "profanities": "^3.0.1", "prom-client": "^15.1.3", "pug": "^3.0.3", @@ -3135,7 +3135,6 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "dev": true, "license": "MIT", "funding": { "type": "opencollective", @@ -5773,9 +5772,9 @@ "license": "ISC" }, "node_modules/bootstrap": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", - "integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==", + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.5.tgz", + "integrity": "sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==", "funding": [ { "type": "github", @@ -5788,8 +5787,7 @@ ], "license": "MIT", "peerDependencies": { - "jquery": "1.9.1 - 3", - "popper.js": "^1.16.1" + "@popperjs/core": "^2.11.8" } }, "node_modules/bowser": { @@ -10767,17 +10765,6 @@ "node": ">=8" } }, - "node_modules/popper.js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", - "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", - "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", diff --git a/package.json b/package.json index 30e187dba..9c3ebf536 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@sentry/node": "^7.120.3", "@types/semver": "^7.7.0", "big-integer": "^1.6.52", - "bootstrap": "^4.6.2", + "bootstrap": "^5.3.5", "buffer": "^6.0.3", "chart.js": "^4.4.9", "clipboard": "^2.0.11", @@ -57,7 +57,7 @@ "nopt": "^8.1.0", "p-queue": "^8.1.0", "path-browserify": "^1.0.1", - "popper.js": "^1.16.1", + "@popperjs/core": "^2.11.8", "profanities": "^3.0.1", "prom-client": "^15.1.3", "pug": "^3.0.3", diff --git a/static/bootstrap-utils.ts b/static/bootstrap-utils.ts new file mode 100644 index 000000000..7e3963f36 --- /dev/null +++ b/static/bootstrap-utils.ts @@ -0,0 +1,471 @@ +// Copyright (c) 2025, 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. + +/** + * TEMPORARY COMPATIBILITY LAYER + * + * This module provides utilities to help transition from Bootstrap 4's jQuery-based API + * to Bootstrap 5's vanilla JavaScript API. This is intended as a temporary solution + * during the migration from Bootstrap 4 to 5 and should be removed once the migration + * is complete. + * + * The goal is to minimize changes throughout the codebase by centralizing the Bootstrap + * API changes in this file, while still allowing for gradual migration to direct API calls. + * + * @deprecated This module should be removed after the Bootstrap 5 migration is complete. + */ + +import $ from 'jquery'; + +import 'bootstrap'; +import {Collapse, Dropdown, Modal, Popover, Tab, Toast, Tooltip} from 'bootstrap'; + +// Private event listener tracking map +const eventListenerMap = new WeakMap>(); + +/** + * Helper method to get an HTMLElement from various input types + * @param elementOrSelector Element, jQuery object, or selector + * @returns HTMLElement or null + */ +function getElement(elementOrSelector: string | HTMLElement | JQuery): HTMLElement | null { + if (!elementOrSelector) return null; + + if (typeof elementOrSelector === 'string') { + return document.querySelector(elementOrSelector as string); + } + + if (elementOrSelector instanceof HTMLElement) { + return elementOrSelector; + } + + if (elementOrSelector instanceof $) { + return (elementOrSelector as JQuery)[0] as HTMLElement; + } + + return null; +} + +/** + * Private helper that sets an event handler on a single DOM element + * Handles the removal of existing handlers and tracking of the new one + * + * @param domElement The DOM element to attach the event to + * @param eventName The event name (e.g., 'hidden.bs.modal', 'shown.bs.modal', 'click', etc.) + * @param handler The event handler function + */ +function setDomElementEventHandler(domElement: HTMLElement, eventName: string, handler: (event: Event) => void): void { + // Initialize nested map structure if needed + if (!eventListenerMap.has(domElement)) { + eventListenerMap.set(domElement, new Map()); + } + + const elementEvents = eventListenerMap.get(domElement)!; + + // Remove existing handler if present + if (elementEvents.has(eventName)) { + const oldHandler = elementEvents.get(eventName)!; + domElement.removeEventListener(eventName, oldHandler); + } + + // Store and add new handler + elementEvents.set(eventName, handler); + domElement.addEventListener(eventName, handler); +} + +/** + * Registers an event handler on element(s), removing any previous handler for the same event + * Similar to jQuery's off().on() pattern + * Works with both single elements and jQuery collections with multiple elements + * + * @param element The element(s) or jQuery object to attach the event to + * @param eventName The event name (e.g., 'hidden.bs.modal', 'shown.bs.modal', 'click', etc.) + * @param handler The event handler function + */ +export function setElementEventHandler( + element: JQuery | HTMLElement, + eventName: string, + handler: (event: Event) => void, +): void { + // If jQuery object with potentially multiple elements + if (!(element instanceof HTMLElement)) { + // Loop through all elements in the jQuery collection + element.each((_index, domElement) => { + setDomElementEventHandler(domElement, eventName, handler); + }); + return; + } + + // Otherwise it's a single DOM element + setDomElementEventHandler(element, eventName, handler); +} + +/** + * Initialize a modal + * @param elementOrSelector Element or selector for the modal + * @param options Modal options + * @returns Modal instance + * @throws Error if the element cannot be found + */ +export function initModal(elementOrSelector: string | HTMLElement | JQuery, options?: Partial): Modal { + const element = getElement(elementOrSelector); + if (!element) throw new Error(`Failed to find element for modal: ${elementOrSelector}`); + + return new Modal(element, options); +} + +/** + * Initialize a modal if the element exists, returning null otherwise + * @param elementOrSelector Element or selector for the modal + * @param options Modal options + * @returns Modal instance or null if the element cannot be found + */ +export function initModalIfExists( + elementOrSelector: string | HTMLElement | JQuery, + options?: Partial, +): Modal | null { + const element = getElement(elementOrSelector); + if (!element) return null; + + return new Modal(element, options); +} + +/** + * Get an existing modal instance for an element + * @param elementOrSelector Element or selector for the modal + * @returns Existing modal instance or null if not found + */ +export function getModalInstance(elementOrSelector: string | HTMLElement | JQuery): Modal | null { + const element = getElement(elementOrSelector); + if (!element) return null; + + return Modal.getInstance(element); +} + +/** + * Show a modal + * @param elementOrSelector Element or selector for the modal + * @param relatedTarget Optional related target element + */ +export function showModal(elementOrSelector: string | HTMLElement | JQuery, relatedTarget?: HTMLElement): void { + const element = getElement(elementOrSelector); + if (!element) return; + + const modal = Modal.getInstance(element) || new Modal(element); + modal.show(relatedTarget); +} + +/** + * Hide a modal + * @param elementOrSelector Element or selector for the modal + */ +export function hideModal(elementOrSelector: string | HTMLElement | JQuery): void { + const element = getElement(elementOrSelector); + if (!element) return; + + const modal = Modal.getInstance(element); + if (modal) modal.hide(); +} + +/** + * Initialize a toast + * @param elementOrSelector Element or selector for the toast + * @param options Toast options + * @returns Toast instance + */ +export function initToast(elementOrSelector: string | HTMLElement | JQuery, options?: Partial): Toast { + const element = getElement(elementOrSelector); + if (!element) throw new Error(`Failed to find element for toast: ${elementOrSelector}`); + + return new Toast(element, options); +} + +/** + * Initialize a toast if the element exists + * @param elementOrSelector Element or selector for the toast + * @param options Toast options + * @returns Toast instance or null if element doesn't exist + */ +export function initToastIfExists( + elementOrSelector: string | HTMLElement | JQuery, + options?: Partial, +): Toast | null { + const element = getElement(elementOrSelector); + if (!element) return null; + + return new Toast(element, options); +} + +/** + * Show a toast + * @param elementOrSelector Element or selector for the toast + */ +export function showToast(elementOrSelector: string | HTMLElement | JQuery): void { + const element = getElement(elementOrSelector); + if (!element) return; + + const toast = Toast.getInstance(element) || new Toast(element); + toast.show(); +} + +/** + * Hide a toast + * @param elementOrSelector Element or selector for the toast + */ +export function hideToast(elementOrSelector: string | HTMLElement | JQuery): void { + const element = getElement(elementOrSelector); + if (!element) return; + + const toast = Toast.getInstance(element); + if (toast) toast.hide(); +} + +/** + * Initialize a dropdown + * @param elementOrSelector Element or selector for the dropdown + * @param options Dropdown options + * @returns Dropdown instance + * @throws Error if the element cannot be found + */ +export function initDropdown( + elementOrSelector: string | HTMLElement | JQuery, + options?: Partial, +): Dropdown { + const element = getElement(elementOrSelector); + if (!element) throw new Error(`Failed to find element for dropdown: ${elementOrSelector}`); + + return new Dropdown(element, options); +} + +/** + * Initialize a dropdown if the element exists, returning null otherwise + * @param elementOrSelector Element or selector for the dropdown + * @param options Dropdown options + * @returns Dropdown instance or null if the element cannot be found + */ +export function initDropdownIfExists( + elementOrSelector: string | HTMLElement | JQuery, + options?: Partial, +): Dropdown | null { + const element = getElement(elementOrSelector); + if (!element) return null; + + return new Dropdown(element, options); +} + +/** + * Get an existing dropdown instance for an element + * @param elementOrSelector Element or selector for the dropdown + * @returns Existing dropdown instance or null if not found + */ +export function getDropdownInstance(elementOrSelector: string | HTMLElement | JQuery): Dropdown | null { + const element = getElement(elementOrSelector); + if (!element) return null; + + return Dropdown.getInstance(element); +} + +/** + * Show a dropdown + * @param elementOrSelector Element or selector for the dropdown + */ +export function showDropdown(elementOrSelector: string | HTMLElement | JQuery): void { + const element = getElement(elementOrSelector); + if (!element) return; + + const dropdown = Dropdown.getInstance(element) || new Dropdown(element); + dropdown.show(); +} + +/** + * Hide a dropdown + * @param elementOrSelector Element or selector for the dropdown + */ +export function hideDropdown(elementOrSelector: string | HTMLElement | JQuery): void { + const element = getElement(elementOrSelector); + if (!element) return; + + const dropdown = Dropdown.getInstance(element); + if (dropdown) dropdown.hide(); +} + +/** + * Initialize a tooltip + * @param elementOrSelector Element or selector for the tooltip + * @param options Tooltip options + * @returns Tooltip instance + */ +export function initTooltip( + elementOrSelector: string | HTMLElement | JQuery, + options?: Partial, +): Tooltip { + const element = getElement(elementOrSelector); + if (!element) throw new Error(`Failed to find element for tooltip: ${elementOrSelector}`); + + return new Tooltip(element, options); +} + +/** + * Initialize a tooltip if the element exists + * @param elementOrSelector Element or selector for the tooltip + * @param options Tooltip options + * @returns Tooltip instance or null if element doesn't exist + */ +export function initTooltipIfExists( + elementOrSelector: string | HTMLElement | JQuery, + options?: Partial, +): Tooltip | null { + const element = getElement(elementOrSelector); + if (!element) return null; + + return new Tooltip(element, options); +} + +/** + * Initialize a popover + * @param elementOrSelector Element or selector for the popover + * @param options Popover options + * @returns Popover instance + * @throws Error if the element cannot be found + */ +export function initPopover( + elementOrSelector: string | HTMLElement | JQuery, + options?: Partial, +): Popover { + const element = getElement(elementOrSelector); + if (!element) throw new Error(`Failed to find element for popover: ${elementOrSelector}`); + + return new Popover(element, options); +} + +/** + * Initialize a popover if the element exists, returning null otherwise + * @param elementOrSelector Element or selector for the popover + * @param options Popover options + * @returns Popover instance or null if the element cannot be found + */ +export function initPopoverIfExists( + elementOrSelector: string | HTMLElement | JQuery, + options?: Partial, +): Popover | null { + const element = getElement(elementOrSelector); + if (!element) return null; + + return new Popover(element, options); +} + +/** + * Get an existing popover instance for an element + * @param elementOrSelector Element or selector for the popover + * @returns Existing popover instance or null if not found + */ +export function getPopoverInstance(elementOrSelector: string | HTMLElement | JQuery): Popover | null { + const element = getElement(elementOrSelector); + if (!element) return null; + + return Popover.getInstance(element); +} + +/** + * Initialize a tab + * @param elementOrSelector Element or selector for the tab + * @returns Tab instance + */ +export function initTab(elementOrSelector: string | HTMLElement | JQuery): Tab { + const element = getElement(elementOrSelector); + if (!element) throw new Error(`Failed to find element for tab: ${elementOrSelector}`); + + return new Tab(element); +} + +/** + * Initialize a tab if the element exists + * @param elementOrSelector Element or selector for the tab + * @returns Tab instance or null if element doesn't exist + */ +export function initTabIfExists(elementOrSelector: string | HTMLElement | JQuery): Tab | null { + const element = getElement(elementOrSelector); + if (!element) return null; + + return new Tab(element); +} + +/** + * Initialize a collapse + * @param elementOrSelector Element or selector for the collapse + * @param options Collapse options + * @returns Collapse instance + */ +export function initCollapse( + elementOrSelector: string | HTMLElement | JQuery, + options?: Partial, +): Collapse { + const element = getElement(elementOrSelector); + if (!element) throw new Error(`Failed to find element for collapse: ${elementOrSelector}`); + + return new Collapse(element, options); +} + +/** + * Initialize a collapse if the element exists + * @param elementOrSelector Element or selector for the collapse + * @param options Collapse options + * @returns Collapse instance or null if element doesn't exist + */ +export function initCollapseIfExists( + elementOrSelector: string | HTMLElement | JQuery, + options?: Partial, +): Collapse | null { + const element = getElement(elementOrSelector); + if (!element) return null; + + return new Collapse(element, options); +} + +/** + * Hide an existing popover if it exists + * @param elementOrSelector Element or selector for the popover + */ +export function hidePopover(elementOrSelector: string | HTMLElement | JQuery): void { + const popover = getPopoverInstance(elementOrSelector); + if (popover) popover.hide(); +} + +/** + * Show an existing popover if it exists + * @param elementOrSelector Element or selector for the popover + */ +export function showPopover(elementOrSelector: string | HTMLElement | JQuery): void { + const popover = getPopoverInstance(elementOrSelector); + if (popover) popover.show(); +} + +/** + * Show an existing modal if it exists (uses existing instance only) + * @param elementOrSelector Element or selector for the modal + */ +export function showModalIfExists(elementOrSelector: string | HTMLElement | JQuery): void { + const modal = getModalInstance(elementOrSelector); + if (modal) modal.show(); +} diff --git a/static/main.ts b/static/main.ts index 1043ef6fd..fb1bb9853 100644 --- a/static/main.ts +++ b/static/main.ts @@ -28,7 +28,7 @@ import {SentryCapture, SetupSentry, setSentryLayout} from './sentry.js'; SetupSentry(); import 'whatwg-fetch'; -import 'popper.js'; +import '@popperjs/core'; import 'bootstrap'; import $ from 'jquery'; @@ -62,6 +62,7 @@ import {ComponentConfig, EmptyCompilerState, StateWithId, StateWithLanguage} fro import {CompilerExplorerOptions} from './global.js'; import * as utils from '../shared/common-utils.js'; +import * as BootstrapUtils from './bootstrap-utils.js'; import {ParseFiltersAndOutputOptions} from './features/filters.interfaces.js'; import {localStorage, sessionThenLocalStorage} from './local.js'; import {Printerinator} from './print-view.js'; @@ -79,7 +80,7 @@ if (!window.PRODUCTION && !options.embedded) { //css require('bootstrap/dist/css/bootstrap.min.css'); require('golden-layout/src/css/goldenlayout-base.css'); -require('tom-select/dist/css/tom-select.bootstrap4.css'); +require('tom-select/dist/css/tom-select.bootstrap5.css'); require('./styles/colours.scss'); require('./styles/explorer.scss'); @@ -225,7 +226,7 @@ function setupButtons(options: CompilerExplorerOptions, hub: Hub) { window.location.reload(); }); - $('#history').modal(); + BootstrapUtils.showModal('#history'); }); $('#ui-apply-default-font-scale').on('click', () => { @@ -530,7 +531,7 @@ function initShortlinkInfoButton() { buttonText.html(''); const button = $('.shortlinkInfo'); - button.popover({ + BootstrapUtils.initPopover(button, { html: true, title: 'Link created', content: formatISODate(dt, true), @@ -677,11 +678,13 @@ function start() { setupButtons(options, hub); } - const addDropdown = $('#addDropdown'); - function setupAdd(thing: JQuery, func: () => ComponentConfig) { (layout.createDragSource(thing, func as any) as any)._dragListener.on('dragStart', () => { - addDropdown.dropdown('toggle'); + const addDropdown = unwrap( + BootstrapUtils.getDropdownInstance('#addDropdown'), + 'Dropdown instance not found for #addDropdown', + ); + addDropdown.toggle(); }); thing.on('click', () => { diff --git a/static/motd.ts b/static/motd.ts index 9722fbe7f..e16737e12 100644 --- a/static/motd.ts +++ b/static/motd.ts @@ -30,7 +30,7 @@ function ensureShownMessage(message: string, motdNode: JQuery) { motdNode.find('.content').html(message); motdNode.removeClass('d-none'); motdNode - .find('.close') + .find('.btn-close') .on('click', () => { motdNode.addClass('d-none'); }) diff --git a/static/noscript.ts b/static/noscript.ts index 869bf479f..57e6a9633 100644 --- a/static/noscript.ts +++ b/static/noscript.ts @@ -25,8 +25,8 @@ // This jQuery import needs to be here, because noscript.ts is a different entrypoint than the rest of the code. // See webpack.config.esm.ts -> entry for more details. import $ from 'jquery'; +import '@popperjs/core'; import 'bootstrap'; -import 'popper.js'; import {Toggles} from './widgets/toggles.js'; import './styles/noscript.scss'; diff --git a/static/panes/cfg-view.ts b/static/panes/cfg-view.ts index 65b9a7a5a..09fba4202 100644 --- a/static/panes/cfg-view.ts +++ b/static/panes/cfg-view.ts @@ -26,6 +26,7 @@ import * as fileSaver from 'file-saver'; import $ from 'jquery'; import * as monaco from 'monaco-editor'; import _ from 'underscore'; +import * as BootstrapUtils from '../bootstrap-utils.js'; import {Pane} from './pane.js'; import {Container} from 'golden-layout'; @@ -281,7 +282,9 @@ export class Cfg extends Pane { if (this.tooltipOpen) { if (!e.target.classList.contains('fold') && $(e.target).parents('.popover.in').length === 0) { this.tooltipOpen = false; - $('.fold').popover('hide'); + $('.fold').each((_, element) => { + BootstrapUtils.hidePopover(element); + }); } } }); @@ -389,25 +392,27 @@ export class Cfg extends Pane { }" aria-describedby="wtf">⋯`; } div.innerHTML = lines.join('
'); - for (const fold of div.getElementsByClassName('fold')) { - $(fold) - .popover({ - content: unwrap(fold.getAttribute('data-extra')), - html: true, - placement: 'top', - template: - '', - }) - .on('show.bs.popover', () => { - this.tooltipOpen = true; - }) - .on('hide.bs.popover', () => { - this.tooltipOpen = false; - }); + for (const foldElement of div.getElementsByClassName('fold')) { + const fold = foldElement as HTMLElement; + + BootstrapUtils.initPopover(fold, { + content: unwrap(fold.getAttribute('data-extra')), + html: true, + placement: 'top', + template: + '', + }); + + BootstrapUtils.setElementEventHandler(fold, 'show.bs.popover', () => { + this.tooltipOpen = true; + }); + BootstrapUtils.setElementEventHandler(fold, 'hide.bs.popover', () => { + this.tooltipOpen = false; + }); } // So because this is async there's a race condition here if you rapidly switch functions. // This can be triggered by loading an example program. Because the fix going to be tricky I'll defer @@ -500,7 +505,11 @@ export class Cfg extends Pane { // Display the cfg for the specified function if it exists // This function sets this.state.selectedFunction if the input is non-null and valid async selectFunction(name: string | null) { - $('.fold').popover('dispose'); + $('.fold').each((_, element) => { + const popover = BootstrapUtils.getPopoverInstance(element); + if (popover) popover.dispose(); + // We need to dispose here, not just hide + }); this.blockContainer.innerHTML = ''; this.svg.innerHTML = ''; this.estimatedPNGSize.innerHTML = ''; @@ -669,7 +678,9 @@ export class Cfg extends Pane { const topBarHeight = utils.updateAndCalcTopBarHeight(this.domRoot, this.topBar, this.hideable); this.graphContainer.style.width = `${unwrap(this.domRoot.width())}px`; this.graphContainer.style.height = `${unwrap(this.domRoot.height()) - topBarHeight}px`; - $('.fold').popover('hide'); + $('.fold').each((_, element) => { + BootstrapUtils.hidePopover(element); + }); }); } diff --git a/static/panes/compiler.ts b/static/panes/compiler.ts index fadec4d3a..7e3b4e3b3 100644 --- a/static/panes/compiler.ts +++ b/static/panes/compiler.ts @@ -42,6 +42,7 @@ import { import {CompilerInfo} from '../../types/compiler.interfaces.js'; import {ResultLine} from '../../types/resultline/resultline.interfaces.js'; import {getAssemblyDocumentation} from '../api/api.js'; +import * as BootstrapUtils from '../bootstrap-utils.js'; import * as codeLensHandler from '../codelens-handler.js'; import * as colour from '../colour.js'; import {OptPipelineBackendOptions} from '../compilation/opt-pipeline-output.interfaces.js'; @@ -678,7 +679,7 @@ export class Compiler extends MonacoPane { - newPaneDropdown.dropdown('hide'); + BootstrapUtils.hideDropdown(newPaneDropdown); }; // Note that the .d.ts file lies in more than 1 way! @@ -729,7 +730,7 @@ export class Compiler extends MonacoPane popularArgumentsMenu.dropdown('hide')); + ._dragListener.on('dragStart', () => BootstrapUtils.hideDropdown(popularArgumentsMenu)); this.flagsButton.on('click', () => { const insertPoint = @@ -1868,7 +1869,7 @@ export class Compiler extends MonacoPane { elem.find('#miracle_emulink').on('click', () => { - dialog.modal(); + BootstrapUtils.showModal(dialog); const miracleMenuFrame = dialog.find('#miracleemuframe')[0]; assert(miracleMenuFrame instanceof HTMLIFrameElement); @@ -1896,7 +1897,7 @@ export class Compiler extends MonacoPane { elem.find('#jsspeccy_emulink').on('click', () => { - dialog.modal(); + BootstrapUtils.showModal(dialog); const speccyemuframe = dialog.find('#speccyemuframe')[0]; assert(speccyemuframe instanceof HTMLIFrameElement); @@ -1923,7 +1924,7 @@ export class Compiler extends MonacoPane { elem.find('#emulink').on('click', () => { - dialog.modal(); + BootstrapUtils.showModal(dialog); const jsbeebemuframe = dialog.find('#jsbeebemuframe')[0]; assert(jsbeebemuframe instanceof HTMLIFrameElement); @@ -1950,7 +1951,7 @@ export class Compiler extends MonacoPane { elem.find('#emulink').on('click', () => { - dialog.modal(); + BootstrapUtils.showModal(dialog); const jsnesemuframe = dialog.find('#jsnesemuframe')[0]; assert(jsnesemuframe instanceof HTMLIFrameElement); @@ -2683,7 +2684,7 @@ export class Compiler extends MonacoPane { - this.toolsMenu?.dropdown('hide'); + if (this.toolsMenu) BootstrapUtils.hideDropdown(this.toolsMenu); }; this.toolsMenu.empty(); @@ -3053,14 +3054,14 @@ export class Compiler extends MonacoPane { if (e.which === 27) { - this.libsButton.popover('hide'); + BootstrapUtils.hidePopover(this.libsButton); } }); @@ -3118,7 +3119,7 @@ export class Compiler extends MonacoPane `
${w}
`).join('\n') + '\n' + diff --git a/static/panes/conformance-view.ts b/static/panes/conformance-view.ts index c5b68c9ee..7729c022b 100644 --- a/static/panes/conformance-view.ts +++ b/static/panes/conformance-view.ts @@ -29,6 +29,7 @@ import {escapeHTML, unique} from '../../shared/common-utils.js'; import {CompilationResult} from '../../types/compilation/compilation.interfaces.js'; import {CompilerInfo} from '../../types/compiler.interfaces.js'; import {unwrapString} from '../assert.js'; +import * as BootstrapUtils from '../bootstrap-utils.js'; import {CompilationStatus} from '../compiler-service.interfaces.js'; import {CompilerService} from '../compiler-service.js'; import * as Components from '../components.js'; @@ -100,7 +101,7 @@ export class Conformance extends Pane { // Dismiss the popover on escape. $(document).on('keyup.editable', e => { if (e.which === 27) { - this.libsButton.popover('hide'); + BootstrapUtils.hidePopover(this.libsButton); } }); @@ -114,7 +115,7 @@ export class Conformance extends Pane { elem.has(target as unknown as Element).length === 0 && target.closest('.popover').length === 0 ) { - elem.popover('hide'); + BootstrapUtils.hidePopover(elem); } }); } @@ -150,7 +151,7 @@ export class Conformance extends Pane { this.conformanceContentRoot = this.domRoot.find('.conformance-wrapper'); this.selectorList = this.domRoot.find('.compiler-list'); this.addCompilerButton = this.domRoot.find('.add-compiler'); - this.selectorTemplate = $('#compiler-selector').find('.form-row'); + this.selectorTemplate = $('#compiler-selector').find('.row'); this.topBar = this.domRoot.find('.top-bar'); this.libsButton = this.topBar.find('.show-libs'); this.hideable = this.domRoot.find('.hideable'); @@ -225,15 +226,11 @@ export class Conformance extends Pane { .on('change', onOptionsChange) .on('keyup', onOptionsChange); - newSelector - .find('.close') - .not('.extract-compiler') - .not('.copy-compiler') - .on('click', () => { - this.removeCompilerPicker(newCompilerEntry); - }); + newSelector.find('.close-compiler').on('click', () => { + this.removeCompilerPicker(newCompilerEntry); + }); - newSelector.find('.close.copy-compiler').on('click', () => { + newSelector.find('.copy-compiler').on('click', () => { const config: AddCompilerPickerConfig = { compilerId: newCompilerEntry.picker?.lastCompilerId ?? '', options: newCompilerEntry.optionsField?.val() || '', @@ -301,15 +298,19 @@ export class Conformance extends Pane { ): void {} setCompilationOptionsPopover(element: JQuery | null, content: string): void { - element?.popover('dispose'); - element?.popover({ - content: content || 'No options in use', - template: - '', - }); + if (element) { + const existingPopover = BootstrapUtils.getPopoverInstance(element); + if (existingPopover) existingPopover.dispose(); + + BootstrapUtils.initPopover(element, { + content: content || 'No options in use', + template: + '', + }); + } } removeCompilerPicker(compilerEntry: CompilerEntry): void { diff --git a/static/panes/diff.ts b/static/panes/diff.ts index 70838d313..33a778241 100644 --- a/static/panes/diff.ts +++ b/static/panes/diff.ts @@ -482,10 +482,7 @@ export class Diff extends MonacoPane maxLength - 3) name = name.substring(0, maxLength - 3) + '...'; + const name = compiler.name + ' ' + options; this.compilers[id] = { id: id, name: name, diff --git a/static/panes/editor.ts b/static/panes/editor.ts index 90958dfaf..b6efdb101 100644 --- a/static/panes/editor.ts +++ b/static/panes/editor.ts @@ -29,6 +29,7 @@ import * as monaco from 'monaco-editor'; import * as monacoVim from 'monaco-vim'; import TomSelect from 'tom-select'; import _ from 'underscore'; +import * as BootstrapUtils from '../bootstrap-utils.js'; import * as colour from '../colour.js'; import * as Components from '../components.js'; import * as monacoConfig from '../monaco-config.js'; @@ -472,7 +473,7 @@ export class Editor extends MonacoPane { if ((this.editor as any).vimInUse) { this.disableVim(); @@ -541,7 +542,7 @@ export class Editor extends MonacoPane { - paneAdderDropdown.dropdown('toggle'); + const dropdown = BootstrapUtils.getDropdownInstance(paneAdderDropdown); + if (dropdown) { + dropdown.toggle(); + } }); dragSource.on('click', () => { @@ -1921,7 +1925,7 @@ export class Editor extends MonacoPane' + + '
' + ' { - this.domRoot.find('.add-pane').dropdown('toggle'); + const dropdown = this.domRoot.find('.add-pane'); + const dropdownInstance = BootstrapUtils.getDropdownInstance(dropdown); + if (dropdownInstance) { + dropdownInstance.toggle(); + } }, ); diff --git a/static/real-dark.ts b/static/real-dark.ts index e2bfddebb..d992598bd 100644 --- a/static/real-dark.ts +++ b/static/real-dark.ts @@ -48,7 +48,7 @@ export function setupRealDark(hub: Hub) { $('#settings .theme').val('real-dark').trigger('change'); } }); - $('#true-dark .content .close').on('click', e => { + $('#true-dark .content .dark-close').on('click', e => { local.localStorage.set(localKey, 'hidden'); toggleButton(); toggleOverlay(); diff --git a/static/sharing.ts b/static/sharing.ts index 6717692af..86012cf0a 100644 --- a/static/sharing.ts +++ b/static/sharing.ts @@ -22,18 +22,20 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. +import {Modal, Tooltip} from 'bootstrap'; import ClipboardJS from 'clipboard'; import GoldenLayout from 'golden-layout'; import $ from 'jquery'; import _ from 'underscore'; +import {unwrap} from './assert.js'; +import * as BootstrapUtils from './bootstrap-utils.js'; import {sessionThenLocalStorage} from './local.js'; import {options} from './options.js'; import * as url from './url.js'; -import ClickEvent = JQuery.ClickEvent; -import TriggeredEvent = JQuery.TriggeredEvent; import {SentryCapture} from './sentry.js'; import {Settings, SiteSettings} from './settings.js'; +import ClickEvent = JQuery.ClickEvent; const cloneDeep = require('lodash.clonedeep'); @@ -87,24 +89,31 @@ export class Sharing { private layout: GoldenLayout; private lastState: any; - private share: JQuery; - private shareShort: JQuery; - private shareFull: JQuery; - private shareEmbed: JQuery; + private readonly share: JQuery; + private readonly shareTooltipTarget: JQuery; + private readonly shareShort: JQuery; + private readonly shareFull: JQuery; + private readonly shareEmbed: JQuery; private settings: SiteSettings; private clippyButton: ClipboardJS | null; + private readonly shareLinkDialog: HTMLElement; constructor(layout: any) { this.layout = layout; this.lastState = null; + this.shareLinkDialog = unwrap(document.getElementById('sharelinkdialog'), 'Share modal element not found'); this.share = $('#share'); + this.shareTooltipTarget = $('#share-tooltip-target'); this.shareShort = $('#shareShort'); this.shareFull = $('#shareFull'); this.shareEmbed = $('#shareEmbed'); + [this.shareShort, this.shareFull, this.shareEmbed].forEach(el => + el.on('click', e => BootstrapUtils.showModal(this.shareLinkDialog, e.currentTarget)), + ); this.settings = Settings.getStoredSettings(); this.clippyButton = null; @@ -122,9 +131,9 @@ export class Sharing { }); this.layout.on('stateChanged', this.onStateChanged.bind(this)); - $('#sharelinkdialog') - .on('show.bs.modal', this.onOpenModalPane.bind(this)) - .on('hidden.bs.modal', this.onCloseModalPane.bind(this)); + BootstrapUtils.initModal(this.shareLinkDialog); + this.shareLinkDialog.addEventListener('show.bs.modal', this.onOpenModalPane.bind(this)); + this.shareLinkDialog.addEventListener('hidden.bs.modal', this.onCloseModalPane.bind(this)); this.layout.eventHub.on('settingsChange', (newSettings: SiteSettings) => { this.settings = newSettings; @@ -176,11 +185,16 @@ export class Sharing { } } - private onOpenModalPane(event: TriggeredEvent): void { - // @ts-ignore The property is added by bootstrap - const button = $(event.relatedTarget); - const currentBind = Sharing.bindToLinkType(button.data('bind')); - const modal = $(event.currentTarget); + private onOpenModalPane(event: Event): void { + const modalEvent = event as Modal.Event; + if (!modalEvent.relatedTarget) { + throw new Error('No relatedTarget found in modal event'); + } + + const button = $(modalEvent.relatedTarget); + const bindStr = button.data('bind') as string; + const currentBind = Sharing.bindToLinkType(bindStr); + const modal = $(event.currentTarget as HTMLElement); const socialSharingElements = modal.find('.socialsharing'); const permalink = modal.find('.permalink'); const embedsettings = modal.find('#embedsettings'); @@ -262,15 +276,16 @@ export class Sharing { } } - private onClipButtonPressed(event: ClickEvent, type: LinkType): void { + private onClipButtonPressed(event: ClickEvent, type: LinkType): boolean { // Don't let the modal show up. - // We need this because the button is a child of the dropdown-item with a data-toggle=modal + // We need this because the button is a child of the dropdown-item with a data-bs-toggle=modal if (Sharing.isNavigatorClipboardAvailable()) { - event.stopPropagation(); this.copyLinkTypeToClipboard(type); - // As we prevented bubbling, the dropdown won't close by itself. We need to trigger it manually - this.share.dropdown('hide'); + event.stopPropagation(); + // As we prevented bubbling, the dropdown won't close by itself. + BootstrapUtils.hideDropdown(this.share); } + return false; } private getLinkOfType(type: LinkType): Promise { @@ -278,7 +293,7 @@ export class Sharing { return new Promise((resolve, reject) => { Sharing.getLinks(config, type, (error: any, newUrl: string, extra: string, updateState: boolean) => { if (error || !newUrl) { - this.displayTooltip(this.share, 'Oops, something went wrong'); + this.displayTooltip(this.shareTooltipTarget, 'Oops, something went wrong'); SentryCapture(error, 'Getting short link failed'); reject(); } else { @@ -295,7 +310,7 @@ export class Sharing { const config = this.layout.toConfig(); Sharing.getLinks(config, type, (error: any, newUrl: string, extra: string, updateState: boolean) => { if (error || !newUrl) { - this.displayTooltip(this.share, 'Oops, something went wrong'); + this.displayTooltip(this.shareTooltipTarget, 'Oops, something went wrong'); SentryCapture(error, 'Getting short link failed'); } else { if (updateState) { @@ -306,16 +321,33 @@ export class Sharing { }); } + // TODO we can consider using bootstrap's "Toast" support in future. private displayTooltip(where: JQuery, message: string): void { - where.tooltip('dispose'); - where.tooltip({ - placement: 'bottom', - trigger: 'manual', - title: message, - }); - where.tooltip('show'); - // Manual triggering of tooltips does not hide them automatically. This timeout ensures they do - setTimeout(() => where.tooltip('hide'), 1500); + // First dispose any existing tooltip + const tooltipEl = where[0]; + if (!tooltipEl) return; + + const existingTooltip = Tooltip.getInstance(tooltipEl); + if (existingTooltip) { + existingTooltip.dispose(); + } + + // Create and show new tooltip + try { + const tooltip = BootstrapUtils.initTooltip(tooltipEl, { + placement: 'bottom', + trigger: 'manual', + title: message, + }); + + tooltip.show(); + + // Manual triggering of tooltips does not hide them automatically. This timeout ensures they do + setTimeout(() => tooltip.hide(), 1500); + } catch (e) { + // If element doesn't exist, just silently fail + console.warn('Could not show tooltip:', e); + } } private openShareModalForType(type: LinkType): void { @@ -336,7 +368,7 @@ export class Sharing { if (Sharing.isNavigatorClipboardAvailable()) { navigator.clipboard .writeText(link) - .then(() => this.displayTooltip(this.share, 'Link copied to clipboard')) + .then(() => this.displayTooltip(this.shareTooltipTarget, 'Link copied to clipboard')) .catch(() => this.openShareModalForType(type)); } else { this.openShareModalForType(type); @@ -397,7 +429,7 @@ export class Sharing { ): string { const embedUrl = Sharing.getEmbeddedUrl(config, root, isReadOnly, extraOptions); // The attributes must be double quoted, the full url's rison contains single quotes - return ``; + return ``; } private static getEmbeddedUrl(config: any, root: string, readOnly: boolean, extraOptions: object): string { @@ -455,7 +487,7 @@ export class Sharing { const newElement = baseTemplate.children('a.share-item').clone(); if (service.logoClass) { newElement.prepend( - $('').addClass('dropdown-icon mr-1').addClass(service.logoClass).prop('title', serviceName), + $('').addClass('dropdown-icon me-1').addClass(service.logoClass).prop('title', serviceName), ); } if (service.text) { diff --git a/static/styles/explorer.scss b/static/styles/explorer.scss index a263a488a..38efe7419 100644 --- a/static/styles/explorer.scss +++ b/static/styles/explorer.scss @@ -36,6 +36,11 @@ body { padding: 0; } +// Fix for Bootstrap 5 navbar +.navbar .container-fluid { + padding-left: 0; +} + .navbar-brand img.logo-overlay { position: absolute; top: 0; @@ -75,13 +80,23 @@ body { .compiler-picker { min-width: 14em; + flex-grow: 0.2 !important; // Ensures the compiler picker doesn't flex-grow too much and take up more space +} + +// Prevent TomSelect items from spilling onto multiple lines +.ts-wrapper .item { + white-space: nowrap; + overflow: hidden; } .compiler-picker .ts-input { text-align: center; - border-left: none !important; - border-top-left-radius: 0; - border-bottom-left-radius: 0; + border-left: none !important; /* Fallback for older browsers */ + border-inline-start: none !important; /* Logical property for RTL support */ + border-top-left-radius: 0; /* Fallback for older browsers */ + border-bottom-left-radius: 0; /* Fallback for older browsers */ + border-start-start-radius: 0; /* Logical property for RTL support */ + border-end-start-radius: 0; /* Logical property for RTL support */ } .function-picker { @@ -294,6 +309,12 @@ pre.content.wrap * { } } +#site-template-loader { + .modal-dialog { + min-width: 800px; /* ensure template modal has adequate width */ + } +} + .modal-body { min-height: 200px; overflow-y: auto; /* make body scrollable -> keep Header & Footer onscreen, if possible */ @@ -377,7 +398,7 @@ pre.content.wrap * { max-width: 100% !important; } -.toast-header .close { +.toast-header .btn-close { float: left; margin-right: 5px; } @@ -387,17 +408,25 @@ pre.content.wrap * { } .font-size-list { - min-width: 43px !important; - max-height: 70% !important; - overflow-y: scroll; + min-width: 60px !important; + max-height: 70vh !important; + overflow-y: auto; width: auto; // For my fellow Firefox users scrollbar-width: thin; + padding: 0.25rem 0; } .font-size-list button { margin-left: 0 !important; border-radius: 0; + width: 100%; + text-align: center; + padding: 0.25rem 0.5rem; +} + +.font-size-list button:hover { + background-color: rgba(0, 0, 0, 0.075); } .font-option { @@ -893,17 +922,18 @@ div.populararguments div.dropdown-menu { } .corporate .ces { - font-size: 165%; - height: 9em; + .btn { + --bs-btn-font-size: 24px; + } + height: 20em; } .legendary .ces { - font-size: 165%; - height: 7em; -} - -.legendary .ces button { - padding: 1em; + .btn { + --bs-btn-font-size: 24px; + padding: 1em; + } + height: 20em; } .ces-logo { @@ -1115,7 +1145,7 @@ div.populararguments div.dropdown-menu { margin-right: 2pt; } -span.badge.badge-pill { +span.badge.rounded-pill { margin-left: 2pt; } @@ -1185,11 +1215,42 @@ html[data-theme='one-dark'] { padding-right: 2rem !important; } +/* Add back the dropdown arrow for TomSelect in Bootstrap 5 */ +.ts-wrapper.single .ts-control:not(.input-active)::after { + content: " "; + display: block; + position: absolute; + top: 50%; + right: 15px; + margin-top: -3px; + width: 0; + height: 0; + border-style: solid; + border-width: 5px 5px 0 5px; + border-color: #343a40 transparent transparent transparent; +} + +/* Styling for the dropdown arrow when dropdown is active */ +.ts-wrapper.single.dropdown-active .ts-control::after { + margin-top: -4px; + border-width: 0 5px 5px 5px; + border-color: transparent transparent #343a40 transparent; +} + +// Fix Bootstrap 5's input-group border overlap issues with TomSelect +// Bootstrap applies a -1px margin to elements matching `:not(:first-child)` in input groups +// TomSelect creates a wrapper (.ts-wrapper) around the select element, which becomes a +// child of the input-group. This wrapper gets the negative margin, causing it to shift +// left by 1px and visually overflow its container boundary. +.input-group > .ts-wrapper:not(:first-child) { + margin-left: 0 !important; // Reset Bootstrap's negative margin for TomSelect wrappers +} + .copy-link-btn { padding-top: 0; } -.conformance-wrapper .compiler-list .form-row { +.conformance-wrapper .compiler-list .row { padding-top: 3px; } @@ -1229,8 +1290,10 @@ html[data-theme='one-dark'] { .prepend-options, .picker-popout-button { - border-top-right-radius: 0; - border-bottom-right-radius: 0; + border-top-right-radius: 0; /* Fallback for older browsers */ + border-bottom-right-radius: 0; /* Fallback for older browsers */ + border-start-end-radius: 0; /* Logical property for RTL support */ + border-end-end-radius: 0; /* Logical property for RTL support */ } .picker-popout-button { @@ -1257,7 +1320,9 @@ html[data-theme='one-dark'] { } .popular-arguments-btn { - border-top-right-radius: 0; + border-top-right-radius: 0; /* Fallback for older browsers */ + border-start-end-radius: 0; /* Logical property for RTL support */ + height: 100%; } .panel-compilation { @@ -1315,9 +1380,14 @@ html[data-theme='one-dark'] { > div { padding: 0; flex-shrink: 0; + &.site-templates-list-col { + min-width: 150px; + width: 20%; + } &.site-template-preview-col { flex-grow: 1; flex-shrink: 1; + min-width: 400px; img { width: 1000px; aspect-ratio: 1000 / 800; @@ -1579,11 +1649,11 @@ html[data-theme='one-dark'] { } } - .close { + .dark-close { position: absolute; - bottom: 0px; + bottom: 0; right: -12px; - //background: #e787e7; + padding: 0; background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, #1a1a1a 48%, #1a1a1a 100%); width: 24px; height: 24px; diff --git a/static/styles/themes/dark-theme.scss b/static/styles/themes/dark-theme.scss index b0d513764..f8f08a2b5 100644 --- a/static/styles/themes/dark-theme.scss +++ b/static/styles/themes/dark-theme.scss @@ -28,12 +28,24 @@ body { background-color: #333 !important; } +// Border color adjustments for form elements in dark theme +.form-select, +.form-control, +.input-group-text, +.ts-wrapper .ts-control { + --bs-border-color: #444; +} + input, textarea { color: #eee !important; background-color: #474747; border: 0 !important; + &::placeholder { + color: #888 !important; + } + &:focus { color: #eee !important; background-color: #474747; @@ -166,7 +178,7 @@ textarea.form-control { .conformance-wrapper { background-color: #1e1e1e !important; - .compiler-list .form-row { + .compiler-list .row { border-bottom: 1px solid #3e3e3e; } } @@ -184,7 +196,7 @@ textarea.form-control { background-color: rgba(49, 54, 60, 0.85); } -.custom-select { +.form-select { background-color: #76a1c8 !important; } @@ -196,6 +208,11 @@ textarea.form-control { color: #fff !important; background-color: #23272b !important; } + + &.active { + background-color: #235765 !important; + color: #fff !important; + } } .dropdown-menu { @@ -366,8 +383,8 @@ textarea.form-control { color: #eee !important; background-color: #333 !important; - .close { - color: #fff; + .btn-close { + filter: invert(100%); } } @@ -437,8 +454,8 @@ textarea.form-control { background-color: #aa3333 !important; color: #fff; - .close { - color: #fff; + .btn-close { + filter: invert(100%); } } @@ -451,8 +468,8 @@ textarea.form-control { background-color: #222 !important; color: #fff; - .close { - color: #fff; + .btn-close { + filter: invert(100%); } } diff --git a/static/styles/themes/default-theme.scss b/static/styles/themes/default-theme.scss index 61386e580..c9451e16a 100644 --- a/static/styles/themes/default-theme.scss +++ b/static/styles/themes/default-theme.scss @@ -164,16 +164,16 @@ a.navbar-brand img.logo.normal { .notification-error { background-color: indianred; color: white; - .close { - color: white; + .btn-close { + filter: invert(100%); } } .notification-on { background-color: green; color: #fff; - .close { - color: white; + .btn-close { + filter: invert(100%); } } @@ -346,7 +346,7 @@ div.argmenuitem span.argdescription { color: #007bfd; } -.conformance-wrapper .compiler-list .form-row { +.conformance-wrapper .compiler-list .row { border-bottom: 1px solid #e3e3e3; } @@ -387,6 +387,11 @@ div.argmenuitem span.argdescription { background-color: #dae0e5; } +.dropdown-item.active { + background-color: #007bfd; + color: white; +} + .currentCursorPosition { color: #15a3b9; background-color: darken(rgba(248, 249, 250, 0.85), 3%); diff --git a/static/styles/themes/one-dark-theme.scss b/static/styles/themes/one-dark-theme.scss index 907240dc4..124ea1a35 100644 --- a/static/styles/themes/one-dark-theme.scss +++ b/static/styles/themes/one-dark-theme.scss @@ -39,11 +39,23 @@ body { background-color: $base !important; } +// Border color adjustments for form elements in dark theme +.form-select, +.form-control, +.input-group-text, +.ts-wrapper .ts-control { + --bs-border-color: $dark; +} + input { color: #eee !important; background-color: $lighter; border: 0 !important; + &::placeholder { + color: #888 !important; + } + &:focus { color: #eee !important; background-color: $lightest; @@ -204,7 +216,7 @@ textarea.form-control { .conformance-wrapper { background-color: #1e1e1e !important; - .compiler-list .form-row { + .compiler-list .row { border-bottom: 1px solid #3e3e3e; } } @@ -222,7 +234,7 @@ textarea.form-control { background-color: opacify($dark, 0.8); } -.custom-select { +.form-select { background-color: #dddddd !important; color: #000 !important; } @@ -235,6 +247,11 @@ textarea.form-control { color: #fff !important; background-color: $lighter !important; } + + &.active { + background-color: #405f9d !important; + color: #fff !important; + } } .dropdown-menu { @@ -325,7 +342,7 @@ textarea.form-control { } } -#v-status { +.v-status { color: #eee !important; } @@ -422,8 +439,8 @@ textarea.form-control { color: #eee !important; background-color: $base !important; - .close { - color: #eee; + .btn-close { + filter: invert(90%); } } @@ -493,8 +510,8 @@ textarea.form-control { background-color: #aa3333 !important; color: #fff; - .close { - color: #fff; + .btn-close { + filter: invert(100%); } } @@ -507,8 +524,8 @@ textarea.form-control { background-color: #222 !important; color: #fff; - .close { - color: #fff; + .btn-close { + filter: invert(100%); } } diff --git a/static/styles/themes/pink-theme.scss b/static/styles/themes/pink-theme.scss index e9f040133..184d33801 100644 --- a/static/styles/themes/pink-theme.scss +++ b/static/styles/themes/pink-theme.scss @@ -36,6 +36,14 @@ body { background-color: $base !important; } +// Border color adjustments for form elements +.form-select, +.form-control, +.input-group-text, +.ts-wrapper .ts-control { + --bs-border-color: $dark; +} + input { //color: #000 !important; background-color: $base; @@ -174,7 +182,7 @@ textarea.form-control { .conformance-wrapper { background-color: #1e1e1e !important; - .compiler-list .form-row { + .compiler-list .row { border-bottom: 1px solid #3e3e3e; } } @@ -188,7 +196,7 @@ textarea.form-control { background-color: opacify($dark, 0.8); } -.custom-select { +.form-select { background-color: #76a1c8 !important; } @@ -198,6 +206,11 @@ textarea.form-control { &:hover { background-color: $dark !important; } + + &.active { + background-color: $darker !important; + color: #212529 !important; + } } .dropdown-menu { @@ -383,10 +396,6 @@ textarea.form-control { color: #212529 !important; background-color: $base !important; border-bottom-color: $dark; - - .close { - color: #212529; - } } .nav.nav-tabs { @@ -459,8 +468,8 @@ textarea.form-control { background-color: #aa3333 !important; color: #fff; - .close { - color: #fff; + .btn-close { + filter: invert(100%); } } @@ -473,8 +482,8 @@ textarea.form-control { background-color: #222 !important; color: #fff; - .close { - color: #fff; + .btn-close { + filter: invert(100%); } } diff --git a/static/widgets/alert.ts b/static/widgets/alert.ts index d00fff13a..304659cf0 100644 --- a/static/widgets/alert.ts +++ b/static/widgets/alert.ts @@ -24,6 +24,7 @@ import $ from 'jquery'; +import * as BootstrapUtils from '../bootstrap-utils.js'; import {AlertAskOptions, AlertEnterTextOptions, AlertNotifyOptions} from './alert.interfaces.js'; export class Alert { @@ -57,10 +58,10 @@ export class Alert { modal.toggleClass('error-alert', isError === true); modal.find('.modal-title').html(title); modal.find('.modal-body').html(body); - modal.modal(); + BootstrapUtils.showModal(modal); + if (onClose) { - modal.off('hidden.bs.modal'); - modal.on('hidden.bs.modal', onClose); + BootstrapUtils.setElementEventHandler(modal, 'hidden.bs.modal', onClose); } return modal; } @@ -83,10 +84,10 @@ export class Alert { modal.find('.modal-footer .no').removeClass('btn-link').addClass(askOptions.noClass); } if (askOptions.onClose) { - modal.off('hidden.bs.modal'); - modal.on('hidden.bs.modal', askOptions.onClose); + BootstrapUtils.setElementEventHandler(modal, 'hidden.bs.modal', askOptions.onClose); } - modal.modal(); + + BootstrapUtils.showModal(modal); return modal; } @@ -108,10 +109,8 @@ export class Alert { const newElement = $(`