mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 09:11:06 +00:00
fix(timetable): reopen colour picker
+ some quick fixes to dropdowns again
This commit is contained in:
+2
-1
@@ -52,7 +52,8 @@ export default function ClosePlugin(): Plugin {
|
|||||||
*/
|
*/
|
||||||
closeBundle() {
|
closeBundle() {
|
||||||
console.log("Bundle closed"); // Log successful closure of the bundle
|
console.log("Bundle closed"); // Log successful closure of the bundle
|
||||||
process.exit(0); // Exit with status 0 indicating a successful build
|
// Do not process.exit here — it can mask Vite render errors and break
|
||||||
|
// multi-target builds (`npm run build` runs chrome then firefox).
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import * as plugins from "@/plugins";
|
|||||||
import { main } from "@/seqta/main";
|
import { main } from "@/seqta/main";
|
||||||
import { delay } from "./seqta/utils/delay";
|
import { delay } from "./seqta/utils/delay";
|
||||||
import { initializeHideSensitiveToggle } from "@/seqta/utils/hideSensitiveToggle";
|
import { initializeHideSensitiveToggle } from "@/seqta/utils/hideSensitiveToggle";
|
||||||
|
import { installSeqtaMenuColourPatch } from "@/seqta/utils/patchSeqtaMenuUpdateColours";
|
||||||
|
|
||||||
function registerFetchSeqtaAppLinkListener() {
|
function registerFetchSeqtaAppLinkListener() {
|
||||||
browser.runtime.onMessage.addListener((request, _sender, sendResponse) => {
|
browser.runtime.onMessage.addListener((request, _sender, sendResponse) => {
|
||||||
@@ -46,6 +47,9 @@ if (document.childNodes[1]) {
|
|||||||
document.childNodes[1].textContent?.includes(
|
document.childNodes[1].textContent?.includes(
|
||||||
"Copyright (c) SEQTA Software",
|
"Copyright (c) SEQTA Software",
|
||||||
) ?? false;
|
) ?? false;
|
||||||
|
if (hasSEQTAText) {
|
||||||
|
installSeqtaMenuColourPatch();
|
||||||
|
}
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+50
-4
@@ -34,7 +34,8 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.uiButton.timetable-zoom.iconFamily,
|
button.timetable-zoom.iconFamily,
|
||||||
|
button.bsplus-timetable-control.iconFamily,
|
||||||
.iconFamily {
|
.iconFamily {
|
||||||
font-family: "IconFamily" !important;
|
font-family: "IconFamily" !important;
|
||||||
}
|
}
|
||||||
@@ -219,6 +220,13 @@ select option {
|
|||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Colour picker dialog teardown can leave an empty shell that blocks clicks */
|
||||||
|
.modaliser-container:not(:has(.modaliser > *)) {
|
||||||
|
display: none !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.connectedNotificationsWrapper > div > button > svg > g {
|
.connectedNotificationsWrapper > div > button > svg > g {
|
||||||
fill: var(--theme-primary) !important;
|
fill: var(--theme-primary) !important;
|
||||||
}
|
}
|
||||||
@@ -335,12 +343,18 @@ select option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timetable-zoom,
|
.timetable-zoom,
|
||||||
.timetable-hide {
|
.timetable-hide,
|
||||||
|
.bsplus-timetable-control {
|
||||||
font-size: 14px !important;
|
font-size: 14px !important;
|
||||||
line-height: 1 !important;
|
line-height: 1 !important;
|
||||||
display: inline-flex !important;
|
display: inline-flex !important;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#main > .dashboard {
|
#main > .dashboard {
|
||||||
@@ -848,6 +862,11 @@ ol:has([class*="MessageList__avatar___"] svg) {
|
|||||||
.quickbar .actions [title="Choose a colour"] > svg {
|
.quickbar .actions [title="Choose a colour"] > svg {
|
||||||
scale: 0.9;
|
scale: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quickbar .actions .timetable-edit-quickbar-btn > svg {
|
||||||
|
scale: 0.9;
|
||||||
|
padding-top: 1px;
|
||||||
|
}
|
||||||
.quickbar[data-yiq="light"] .actions {
|
.quickbar[data-yiq="light"] .actions {
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
@@ -1078,7 +1097,13 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
min-height: 128px !important;
|
min-height: 128px !important;
|
||||||
}
|
}
|
||||||
body.student #menu > ul::before {
|
body.student #menu > ul::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
background-image: var(--betterseqta-logo) !important;
|
background-image: var(--betterseqta-logo) !important;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: auto 48px;
|
||||||
position: -webkit-sticky;
|
position: -webkit-sticky;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -2712,11 +2737,24 @@ body {
|
|||||||
.days {
|
.days {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.modaliser {
|
/* Do not hide .modaliser globally — SEQTA Modaliser relies on transitionend to
|
||||||
display: none;
|
dispose; display:none prevents that and leaves empty shells that block clicks. */
|
||||||
|
.modaliser-container:not(.visible) {
|
||||||
|
display: none !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modaliser-container.visible .modaliser {
|
||||||
background: var(--better-main);
|
background: var(--better-main);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ColourChooser teardown can leave a full-screen uiSlidePane that blocks entry clicks */
|
||||||
|
.uiSlidePane:not(.shown):has(.pane.colourChooser) {
|
||||||
|
display: none !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
[class*="MessageList__unread___"] {
|
[class*="MessageList__unread___"] {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--background-secondary, rgb(228 225 225));
|
background: var(--background-secondary, rgb(228 225 225));
|
||||||
@@ -2809,6 +2847,14 @@ body {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Never let a closed Coloris picker intercept timetable clicks */
|
||||||
|
body:not(.clr-open) .clr-picker,
|
||||||
|
.clr-picker:not(.clr-open) {
|
||||||
|
display: none !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
.dark
|
.dark
|
||||||
[class*="MessageList__MessageList___"]
|
[class*="MessageList__MessageList___"]
|
||||||
> ol
|
> ol
|
||||||
|
|||||||
@@ -1,83 +1,170 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
let { state, onChange, options } = $props<{
|
let { value, onChange, options } = $props<{
|
||||||
state: string,
|
value: string,
|
||||||
onChange: (newState: string) => void,
|
onChange: (newValue: string) => void,
|
||||||
options: Array<{ value: string, label: string }>
|
options: Array<{ value: string, label: string }>
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let select: HTMLSelectElement;
|
let isOpen = $state(false);
|
||||||
|
let root: HTMLDivElement | undefined = $state();
|
||||||
|
|
||||||
|
const selectedLabel = $derived(
|
||||||
|
options.find((option) => option.value === value)?.label ?? value,
|
||||||
|
);
|
||||||
|
|
||||||
|
function toggleOpen() {
|
||||||
|
isOpen = !isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectValue(nextValue: string) {
|
||||||
|
onChange(nextValue);
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
const onPointerDown = (event: PointerEvent) => {
|
||||||
|
const path = event.composedPath();
|
||||||
|
if (root && path.includes(root)) return;
|
||||||
|
isOpen = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("pointerdown", onPointerDown, true);
|
||||||
|
return () => document.removeEventListener("pointerdown", onPointerDown, true);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="select-wrapper relative w-full overflow-hidden rounded-2xl border shadow-2xl">
|
<div class="select-wrapper" bind:this={root}>
|
||||||
<select
|
<button
|
||||||
bind:this={select}
|
type="button"
|
||||||
value={state}
|
class="select-trigger"
|
||||||
onchange={() => onChange(select.value)}
|
aria-haspopup="listbox"
|
||||||
class="select-input w-full appearance-none border-none bg-transparent px-4 py-2.5 pr-10 text-[0.875rem] font-medium transition-colors"
|
aria-expanded={isOpen}
|
||||||
|
onclick={toggleOpen}
|
||||||
>
|
>
|
||||||
{#each options as option}
|
<span class="select-label">{selectedLabel}</span>
|
||||||
<option value={option.value}>
|
<span class="select-icon" aria-hidden="true">
|
||||||
{option.label}
|
<svg viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4">
|
||||||
</option>
|
<path
|
||||||
{/each}
|
fill-rule="evenodd"
|
||||||
</select>
|
d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.168l3.71-3.938a.75.75 0 1 1 1.08 1.04l-4.25 4.5a.75.75 0 0 1-1.08 0l-4.25-4.5a.75.75 0 0 1 .02-1.06Z"
|
||||||
<span class="select-icon pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3" aria-hidden="true">
|
clip-rule="evenodd"
|
||||||
<svg viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4">
|
></path>
|
||||||
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.168l3.71-3.938a.75.75 0 1 1 1.08 1.04l-4.25 4.5a.75.75 0 0 1-1.08 0l-4.25-4.5a.75.75 0 0 1 .02-1.06Z" clip-rule="evenodd"></path>
|
</svg>
|
||||||
</svg>
|
</span>
|
||||||
</span>
|
</button>
|
||||||
|
|
||||||
|
{#if isOpen}
|
||||||
|
<ul class="select-menu" role="listbox">
|
||||||
|
{#each options as option (option.value)}
|
||||||
|
<li role="option" aria-selected={option.value === value}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="select-option"
|
||||||
|
class:is-selected={option.value === value}
|
||||||
|
onclick={() => selectValue(option.value)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.select-wrapper {
|
.select-wrapper {
|
||||||
background: color-mix(in srgb, var(--background-primary) 88%, transparent);
|
position: relative;
|
||||||
border-color: color-mix(in srgb, var(--theme-offset-bg, var(--background-secondary)) 72%, transparent);
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--theme-offset-bg, var(--background-secondary)) 72%, transparent);
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
|
background: color-mix(in srgb, var(--background-primary) 88%, transparent);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
padding: 0.625rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.25;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||||
transition:
|
transition:
|
||||||
background-color 180ms ease,
|
background-color 180ms ease,
|
||||||
border-color 180ms ease,
|
border-color 180ms ease,
|
||||||
box-shadow 180ms ease,
|
box-shadow 180ms ease;
|
||||||
transform 180ms ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-wrapper:hover {
|
.select-trigger:hover {
|
||||||
background: color-mix(in srgb, var(--background-primary) 94%, var(--background-secondary) 6%);
|
background: color-mix(in srgb, var(--background-primary) 94%, var(--background-secondary) 6%);
|
||||||
border-color: color-mix(in srgb, var(--theme-offset-bg, var(--background-secondary)) 88%, transparent);
|
border-color: color-mix(in srgb, var(--theme-offset-bg, var(--background-secondary)) 88%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-wrapper:focus-within {
|
.select-trigger:focus-visible {
|
||||||
|
outline: none;
|
||||||
background: color-mix(in srgb, var(--background-primary) 96%, var(--background-secondary) 4%);
|
background: color-mix(in srgb, var(--background-primary) 96%, var(--background-secondary) 4%);
|
||||||
border-color: color-mix(in srgb, var(--text-primary) 22%, var(--theme-offset-bg, var(--background-secondary)) 78%);
|
border-color: color-mix(in srgb, var(--text-primary) 22%, var(--theme-offset-bg, var(--background-secondary)) 78%);
|
||||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--text-primary) 12%, transparent);
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--text-primary) 12%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-input {
|
.select-label {
|
||||||
color: var(--text-primary);
|
overflow: hidden;
|
||||||
outline: none;
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
white-space: nowrap;
|
||||||
|
|
||||||
.select-input:hover,
|
|
||||||
.select-input:focus {
|
|
||||||
background: transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-icon {
|
.select-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
color: color-mix(in srgb, var(--text-primary) 60%, transparent);
|
color: color-mix(in srgb, var(--text-primary) 60%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-input {
|
.select-menu {
|
||||||
color-scheme: light;
|
position: absolute;
|
||||||
|
top: calc(100% + 0.35rem);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 50;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.35rem;
|
||||||
|
list-style: none;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--theme-offset-bg, var(--background-secondary)) 72%, transparent);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--background-primary);
|
||||||
|
box-shadow:
|
||||||
|
0 10px 25px -5px rgb(0 0 0 / 0.25),
|
||||||
|
0 8px 10px -6px rgb(0 0 0 / 0.2);
|
||||||
|
max-height: 16rem;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .select-input {
|
.select-option {
|
||||||
color-scheme: dark;
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 150ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Native option lists on Windows/Edge often stay light regardless of color-scheme */
|
.select-option:hover,
|
||||||
.select-input option {
|
.select-option:focus-visible {
|
||||||
background-color: #ffffff;
|
outline: none;
|
||||||
color: #18181b;
|
background: color-mix(in srgb, var(--background-secondary) 55%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-option.is-selected {
|
||||||
|
background: color-mix(in srgb, var(--background-secondary) 70%, transparent);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -249,7 +249,7 @@
|
|||||||
id: 10,
|
id: 10,
|
||||||
Component: Select,
|
Component: Select,
|
||||||
props: {
|
props: {
|
||||||
state: $settingsState.defaultPage ?? "home",
|
value: $settingsState.defaultPage ?? "home",
|
||||||
onChange: (value: string) => (settingsState.defaultPage = value),
|
onChange: (value: string) => (settingsState.defaultPage = value),
|
||||||
options: [
|
options: [
|
||||||
{ value: "home", label: "Home" },
|
{ value: "home", label: "Home" },
|
||||||
@@ -268,7 +268,7 @@
|
|||||||
id: 11,
|
id: 11,
|
||||||
Component: Select,
|
Component: Select,
|
||||||
props: {
|
props: {
|
||||||
state: $settingsState.newsSource,
|
value: $settingsState.newsSource,
|
||||||
onChange: (value: string) => settingsState.newsSource = value,
|
onChange: (value: string) => settingsState.newsSource = value,
|
||||||
options: [
|
options: [
|
||||||
{ value: "australia", label: "Australia" },
|
{ value: "australia", label: "Australia" },
|
||||||
@@ -405,7 +405,7 @@
|
|||||||
/>
|
/>
|
||||||
{:else if setting.type === 'select'}
|
{:else if setting.type === 'select'}
|
||||||
<Select
|
<Select
|
||||||
state={pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default}
|
value={pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default}
|
||||||
onChange={(value) => updatePluginSetting(plugin.pluginId, key, value)}
|
onChange={(value) => updatePluginSetting(plugin.pluginId, key, value)}
|
||||||
options={(setting.options as string[]).map(opt => ({
|
options={(setting.options as string[]).map(opt => ({
|
||||||
value: opt,
|
value: opt,
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import browser from "webextension-polyfill";
|
||||||
|
|
||||||
|
/** Vite `?url` imports are already absolute extension URLs in production bundles. */
|
||||||
|
export function resolveExtensionAssetUrl(importedUrl: string): string {
|
||||||
|
if (/^(chrome-extension|moz-extension|https?):/.test(importedUrl)) {
|
||||||
|
return importedUrl;
|
||||||
|
}
|
||||||
|
return browser.runtime.getURL(importedUrl.replace(/^\/+/, ""));
|
||||||
|
}
|
||||||
@@ -3,12 +3,14 @@ import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json";
|
|||||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||||
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
|
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
|
||||||
import {
|
import {
|
||||||
ChangeMenuItemPositions,
|
|
||||||
ensureAnalyticsMenuOrder,
|
ensureAnalyticsMenuOrder,
|
||||||
insertMenuItemAfterKey,
|
insertMenuItemAfterKey,
|
||||||
processMenuItemNode,
|
processMenuItemNode,
|
||||||
} from "@/seqta/utils/sidebarMenuIcons";
|
} from "@/seqta/utils/sidebarMenuIcons";
|
||||||
import { MenuOptionsOpen } from "@/seqta/utils/Openers/OpenMenuOptions";
|
import {
|
||||||
|
ChangeMenuItemPositions,
|
||||||
|
MenuOptionsOpen,
|
||||||
|
} from "@/seqta/utils/Openers/OpenMenuOptions";
|
||||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
import {
|
import {
|
||||||
applyMenuItemVisibility,
|
applyMenuItemVisibility,
|
||||||
|
|||||||
@@ -885,7 +885,7 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-analytics-chart-cell > :global(.bsplus-analytics-card) {
|
.bsplus-analytics-chart-cell > .bsplus-analytics-card {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
import type { Plugin } from "../../core/types";
|
import type { Plugin } from "../../core/types";
|
||||||
import {
|
import { attachTimetableColorisRecovery } from "@/seqta/utils/timetableColoris";
|
||||||
attachTimetableColorisRecovery,
|
import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat";
|
||||||
resetStuckColorisPicker,
|
|
||||||
} from "@/seqta/utils/timetableColoris";
|
|
||||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||||
|
|
||||||
const timetablePlugin: Plugin<{}, {}> = {
|
const timetablePlugin: Plugin<{}, {}> = {
|
||||||
@@ -67,7 +65,6 @@ function resetTimetableStyles(): void {
|
|||||||
|
|
||||||
async function handleTimetable(): Promise<void> {
|
async function handleTimetable(): Promise<void> {
|
||||||
attachTimetableColorisRecovery();
|
attachTimetableColorisRecovery();
|
||||||
resetStuckColorisPicker();
|
|
||||||
|
|
||||||
// SEQTA uses `.times` blocks on entries, not necessarily `.time`; avoid infinite polling on a missing selector.
|
// SEQTA uses `.times` blocks on entries, not necessarily `.time`; avoid infinite polling on a missing selector.
|
||||||
try {
|
try {
|
||||||
@@ -97,11 +94,13 @@ function handleTimetableZoom(): void {
|
|||||||
zoomControls.className = "timetable-zoom-controls";
|
zoomControls.className = "timetable-zoom-controls";
|
||||||
|
|
||||||
const zoomIn = document.createElement("button");
|
const zoomIn = document.createElement("button");
|
||||||
zoomIn.className = "uiButton timetable-zoom iconFamily";
|
zoomIn.type = "button";
|
||||||
|
zoomIn.className = "timetable-zoom iconFamily bsplus-timetable-control";
|
||||||
zoomIn.innerHTML = ""; // Unicode for zoom in icon (custom iconfamily)
|
zoomIn.innerHTML = ""; // Unicode for zoom in icon (custom iconfamily)
|
||||||
|
|
||||||
const zoomOut = document.createElement("button");
|
const zoomOut = document.createElement("button");
|
||||||
zoomOut.className = "uiButton timetable-zoom iconFamily";
|
zoomOut.type = "button";
|
||||||
|
zoomOut.className = "timetable-zoom iconFamily bsplus-timetable-control";
|
||||||
zoomOut.innerHTML = ""; // Unicode for zoom out icon (custom iconfamily)
|
zoomOut.innerHTML = ""; // Unicode for zoom out icon (custom iconfamily)
|
||||||
|
|
||||||
zoomControls.appendChild(zoomOut);
|
zoomControls.appendChild(zoomOut);
|
||||||
@@ -140,7 +139,8 @@ function handleTimetableAssessmentHide(): void {
|
|||||||
hideControls.className = "timetable-hide-controls";
|
hideControls.className = "timetable-hide-controls";
|
||||||
|
|
||||||
const hideOn = document.createElement("button");
|
const hideOn = document.createElement("button");
|
||||||
hideOn.className = "uiButton timetable-hide iconFamily";
|
hideOn.type = "button";
|
||||||
|
hideOn.className = "timetable-hide iconFamily bsplus-timetable-control";
|
||||||
hideOn.innerHTML = "";
|
hideOn.innerHTML = "";
|
||||||
|
|
||||||
hideControls.appendChild(hideOn);
|
hideControls.appendChild(hideOn);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ function getRoomAndTeacherElements(entry: HTMLElement): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EDIT_ICON_SVG =
|
const EDIT_ICON_SVG =
|
||||||
'<svg width="24" height="24" viewBox="0 0 24 24"><g style="fill: currentcolor;"><path d="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z"/></g></svg>';
|
'<svg width="20" height="20" viewBox="0 0 24 24"><g style="fill: currentcolor;"><path d="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z"/></g></svg>';
|
||||||
|
|
||||||
function showEditModal(
|
function showEditModal(
|
||||||
item: TimetableEntryData,
|
item: TimetableEntryData,
|
||||||
@@ -147,7 +147,11 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
|||||||
let quickbarObserver: MutationObserver | null = null;
|
let quickbarObserver: MutationObserver | null = null;
|
||||||
let quickbarSyncTimer: ReturnType<typeof setTimeout> | null = null;
|
let quickbarSyncTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
let lastClickedCi: number | null = null;
|
let lastClickedCi: number | null = null;
|
||||||
let lastClickedEntry: { roomEl: HTMLElement; teacherEl: HTMLElement; item: TimetableEntryData } | null = null;
|
let lastClickedEntry: {
|
||||||
|
roomEl: HTMLElement | null;
|
||||||
|
teacherEl: HTMLElement | null;
|
||||||
|
item: TimetableEntryData;
|
||||||
|
} | null = null;
|
||||||
let lastSyncedQuickbarCi: number | null = null;
|
let lastSyncedQuickbarCi: number | null = null;
|
||||||
|
|
||||||
const getOverrides = (): TimetableOverrides =>
|
const getOverrides = (): TimetableOverrides =>
|
||||||
@@ -161,6 +165,64 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
|||||||
): { room?: string; staff?: string } | undefined =>
|
): { room?: string; staff?: string } | undefined =>
|
||||||
getOverrides()[String(ci)] ?? getOverridesBySubject()[description];
|
getOverrides()[String(ci)] ?? getOverridesBySubject()[description];
|
||||||
|
|
||||||
|
const findClassEntry = (
|
||||||
|
title: string,
|
||||||
|
calendarId?: string | null,
|
||||||
|
): HTMLElement | null => {
|
||||||
|
if (calendarId) {
|
||||||
|
const byCalendar = document.querySelector(
|
||||||
|
`.timetablepage .entry.class[data-calendarid="${calendarId}"]`,
|
||||||
|
);
|
||||||
|
if (byCalendar) return byCalendar as HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of document.querySelectorAll(".timetablepage .entry.class")) {
|
||||||
|
const entryTitle = entry.querySelector(".title")?.textContent?.trim();
|
||||||
|
if (entryTitle === title) return entry as HTMLElement;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveContextFromQuickbar = (quickbar: HTMLElement): void => {
|
||||||
|
const title = quickbar.querySelector(".title")?.textContent?.trim() ?? "";
|
||||||
|
if (!title) return;
|
||||||
|
|
||||||
|
const quickbarRoom = quickbar.querySelector(".meta .room")?.textContent?.trim() ?? "";
|
||||||
|
const quickbarStaff =
|
||||||
|
quickbar.querySelector(".meta .teacher")?.textContent?.trim() ?? "";
|
||||||
|
|
||||||
|
const entry = findClassEntry(title);
|
||||||
|
if (entry) {
|
||||||
|
const ciStr = entry.getAttribute("data-instance");
|
||||||
|
const ci = ciStr ? parseInt(ciStr, 10) : NaN;
|
||||||
|
const { roomEl, teacherEl } = getRoomAndTeacherElements(entry);
|
||||||
|
const description = title;
|
||||||
|
const room = roomEl?.textContent?.trim() ?? quickbarRoom;
|
||||||
|
const staff = teacherEl?.textContent?.trim() ?? quickbarStaff;
|
||||||
|
lastClickedCi = isNaN(ci) ? null : ci;
|
||||||
|
lastClickedEntry = {
|
||||||
|
roomEl,
|
||||||
|
teacherEl,
|
||||||
|
item: { ci: isNaN(ci) ? 0 : ci, description, room, staff },
|
||||||
|
};
|
||||||
|
lastSyncedQuickbarCi = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastClickedCi = null;
|
||||||
|
lastClickedEntry = {
|
||||||
|
roomEl: null,
|
||||||
|
teacherEl: null,
|
||||||
|
item: {
|
||||||
|
ci: 0,
|
||||||
|
description: title,
|
||||||
|
room: quickbarRoom,
|
||||||
|
staff: quickbarStaff,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
lastSyncedQuickbarCi = null;
|
||||||
|
};
|
||||||
|
|
||||||
const processEntry = (entry: HTMLElement): void => {
|
const processEntry = (entry: HTMLElement): void => {
|
||||||
if (entry.classList.contains("assessment") || entry.hasAttribute("data-timetable-edit-processed")) return;
|
if (entry.classList.contains("assessment") || entry.hasAttribute("data-timetable-edit-processed")) return;
|
||||||
|
|
||||||
@@ -212,16 +274,20 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const applyOverridesToQuickbar = (quickbar: HTMLElement): void => {
|
const applyOverridesToQuickbar = (quickbar: HTMLElement): void => {
|
||||||
if (lastClickedCi === null) return;
|
resolveContextFromQuickbar(quickbar);
|
||||||
if (lastSyncedQuickbarCi === lastClickedCi) return;
|
|
||||||
|
|
||||||
const description =
|
const description =
|
||||||
quickbar.querySelector(".title")?.textContent?.trim() ??
|
quickbar.querySelector(".title")?.textContent?.trim() ??
|
||||||
lastClickedEntry?.item.description ??
|
lastClickedEntry?.item.description ??
|
||||||
"";
|
"";
|
||||||
const override = getEffectiveOverride(lastClickedCi, description);
|
if (!description) return;
|
||||||
|
|
||||||
|
const ci = lastClickedCi ?? lastClickedEntry?.item.ci ?? 0;
|
||||||
|
if (lastSyncedQuickbarCi === ci && lastClickedCi !== null) return;
|
||||||
|
|
||||||
|
const override = getEffectiveOverride(ci, description);
|
||||||
if (!override) {
|
if (!override) {
|
||||||
lastSyncedQuickbarCi = lastClickedCi;
|
lastSyncedQuickbarCi = ci;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +303,7 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
|||||||
teacherEl.textContent = override.staff;
|
teacherEl.textContent = override.staff;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastSyncedQuickbarCi = lastClickedCi;
|
lastSyncedQuickbarCi = ci;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateVisibleQuickbar = (room: string, staff: string): void => {
|
const updateVisibleQuickbar = (room: string, staff: string): void => {
|
||||||
@@ -259,13 +325,21 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
|||||||
if (quickbarSyncTimer !== null) clearTimeout(quickbarSyncTimer);
|
if (quickbarSyncTimer !== null) clearTimeout(quickbarSyncTimer);
|
||||||
|
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
|
const maxAttempts = 15;
|
||||||
const trySync = (): void => {
|
const trySync = (): void => {
|
||||||
const quickbar = getVisibleClassQuickbar();
|
const quickbar = getVisibleClassQuickbar();
|
||||||
if (quickbar && lastClickedCi !== null) {
|
if (!quickbar) {
|
||||||
syncClassQuickbar(quickbar);
|
if (++attempts < maxAttempts) {
|
||||||
|
quickbarSyncTimer = setTimeout(trySync, 50);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (++attempts < 6) {
|
|
||||||
|
syncClassQuickbar(quickbar);
|
||||||
|
|
||||||
|
const hasButton = quickbar.querySelector(".timetable-edit-quickbar-btn");
|
||||||
|
const hasActions = quickbar.querySelector(".actions");
|
||||||
|
if ((!hasButton || !hasActions) && ++attempts < maxAttempts) {
|
||||||
quickbarSyncTimer = setTimeout(trySync, 50);
|
quickbarSyncTimer = setTimeout(trySync, 50);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -279,24 +353,33 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
|||||||
const actions = quickbar.querySelector(".actions");
|
const actions = quickbar.querySelector(".actions");
|
||||||
if (!actions) return;
|
if (!actions) return;
|
||||||
|
|
||||||
|
const colourBtn = actions.querySelector(
|
||||||
|
"[title='Choose a colour'], button.uiButton",
|
||||||
|
);
|
||||||
const btn = document.createElement("button");
|
const btn = document.createElement("button");
|
||||||
btn.type = "button";
|
btn.type = "button";
|
||||||
btn.className = "uiButton timetable-edit-quickbar-btn";
|
btn.className =
|
||||||
|
colourBtn instanceof HTMLElement
|
||||||
|
? `${colourBtn.className} timetable-edit-quickbar-btn`
|
||||||
|
: "timetable-edit-quickbar-btn";
|
||||||
btn.title = "Edit room and teacher";
|
btn.title = "Edit room and teacher";
|
||||||
btn.innerHTML = EDIT_ICON_SVG;
|
btn.innerHTML = EDIT_ICON_SVG;
|
||||||
|
|
||||||
btn.addEventListener("click", (e) => {
|
btn.addEventListener("click", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const ci = lastClickedCi;
|
|
||||||
const entryData = lastClickedEntry;
|
|
||||||
if (!ci || !entryData) return;
|
|
||||||
|
|
||||||
const qb = (e.currentTarget as HTMLElement).closest(".quickbar");
|
const qb = (e.currentTarget as HTMLElement).closest(".quickbar");
|
||||||
if (!qb) return;
|
if (!qb) return;
|
||||||
|
|
||||||
|
resolveContextFromQuickbar(qb as HTMLElement);
|
||||||
|
const entryData = lastClickedEntry;
|
||||||
|
if (!entryData) return;
|
||||||
|
|
||||||
const quickbarRoom = qb.querySelector(".meta .room")?.textContent?.trim() ?? "";
|
const quickbarRoom = qb.querySelector(".meta .room")?.textContent?.trim() ?? "";
|
||||||
const quickbarTeacher = qb.querySelector(".meta .teacher")?.textContent?.trim() ?? "";
|
const quickbarTeacher = qb.querySelector(".meta .teacher")?.textContent?.trim() ?? "";
|
||||||
const quickbarTitle = qb.querySelector(".title")?.textContent?.trim() ?? "";
|
const quickbarTitle = qb.querySelector(".title")?.textContent?.trim() ?? "";
|
||||||
|
const ci = lastClickedCi ?? entryData.item.ci;
|
||||||
const item: TimetableEntryData = {
|
const item: TimetableEntryData = {
|
||||||
ci,
|
ci,
|
||||||
description: quickbarTitle || entryData.item.description,
|
description: quickbarTitle || entryData.item.description,
|
||||||
@@ -308,8 +391,8 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
|||||||
item,
|
item,
|
||||||
getOverrides(),
|
getOverrides(),
|
||||||
getOverridesBySubject(),
|
getOverridesBySubject(),
|
||||||
(ci, room, staff, applyToFuture) => {
|
(saveCi, room, staff, applyToFuture) => {
|
||||||
if (applyToFuture) {
|
if (applyToFuture || !saveCi) {
|
||||||
const bySubject = { ...getOverridesBySubject() };
|
const bySubject = { ...getOverridesBySubject() };
|
||||||
bySubject[item.description] = {
|
bySubject[item.description] = {
|
||||||
room: room || undefined,
|
room: room || undefined,
|
||||||
@@ -320,7 +403,7 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
|||||||
const current = getOverrides();
|
const current = getOverrides();
|
||||||
api.storage.timetableOverrides = {
|
api.storage.timetableOverrides = {
|
||||||
...current,
|
...current,
|
||||||
[String(ci)]: { room: room || undefined, staff: staff || undefined },
|
[String(saveCi)]: { room: room || undefined, staff: staff || undefined },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (entryData.roomEl) entryData.roomEl.textContent = room;
|
if (entryData.roomEl) entryData.roomEl.textContent = room;
|
||||||
@@ -328,10 +411,12 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
|||||||
updateVisibleQuickbar(room, staff);
|
updateVisibleQuickbar(room, staff);
|
||||||
processAllEntries();
|
processAllEntries();
|
||||||
},
|
},
|
||||||
(ci) => {
|
(clearCi) => {
|
||||||
const current = getOverrides();
|
if (clearCi) {
|
||||||
delete current[String(ci)];
|
const current = getOverrides();
|
||||||
api.storage.timetableOverrides = current;
|
delete current[String(clearCi)];
|
||||||
|
api.storage.timetableOverrides = current;
|
||||||
|
}
|
||||||
const bySubject = getOverridesBySubject();
|
const bySubject = getOverridesBySubject();
|
||||||
delete bySubject[item.description];
|
delete bySubject[item.description];
|
||||||
api.storage.timetableOverridesBySubject = bySubject;
|
api.storage.timetableOverridesBySubject = bySubject;
|
||||||
@@ -343,12 +428,15 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
actions.insertBefore(btn, actions.firstChild);
|
actions.insertBefore(
|
||||||
|
btn,
|
||||||
|
colourBtn ?? actions.firstChild,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncQuickbarFromDOM = () => {
|
const syncQuickbarFromDOM = () => {
|
||||||
const quickbar = getVisibleClassQuickbar();
|
const quickbar = getVisibleClassQuickbar();
|
||||||
if (!quickbar || lastClickedCi === null || !lastClickedEntry) return;
|
if (!quickbar) return;
|
||||||
syncClassQuickbar(quickbar);
|
syncClassQuickbar(quickbar);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -357,26 +445,41 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
|||||||
if (!timetablePage || quickbarObserver) return;
|
if (!timetablePage || quickbarObserver) return;
|
||||||
|
|
||||||
quickbarObserver = new MutationObserver((mutations) => {
|
quickbarObserver = new MutationObserver((mutations) => {
|
||||||
const quickbarBecameVisible = mutations.some(
|
|
||||||
(mutation) =>
|
|
||||||
mutation.type === "attributes" &&
|
|
||||||
mutation.attributeName === "class" &&
|
|
||||||
(mutation.target as HTMLElement).classList.contains("quickbar") &&
|
|
||||||
(mutation.target as HTMLElement).classList.contains("visible"),
|
|
||||||
);
|
|
||||||
if (!quickbarBecameVisible || lastClickedCi === null) return;
|
|
||||||
|
|
||||||
const quickbar = getVisibleClassQuickbar();
|
const quickbar = getVisibleClassQuickbar();
|
||||||
if (quickbar) syncClassQuickbar(quickbar);
|
if (!quickbar) return;
|
||||||
|
|
||||||
|
const shouldSync = mutations.some((mutation) => {
|
||||||
|
if (mutation.type === "attributes" && mutation.attributeName === "class") {
|
||||||
|
const target = mutation.target as HTMLElement;
|
||||||
|
return target.classList.contains("quickbar");
|
||||||
|
}
|
||||||
|
if (mutation.type === "childList") {
|
||||||
|
const target = mutation.target as HTMLElement;
|
||||||
|
return target.classList?.contains("quickbar") || target.closest?.(".quickbar");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (shouldSync) scheduleQuickbarSync();
|
||||||
});
|
});
|
||||||
|
|
||||||
quickbarObserver.observe(timetablePage, {
|
quickbarObserver.observe(timetablePage, {
|
||||||
subtree: true,
|
subtree: true,
|
||||||
|
childList: true,
|
||||||
attributes: true,
|
attributes: true,
|
||||||
attributeFilter: ["class"],
|
attributeFilter: ["class"],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onTimetableEntryClick = (event: Event): void => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
if (!target.closest?.(".timetablepage .entry.class")) return;
|
||||||
|
lastSyncedQuickbarCi = null;
|
||||||
|
scheduleQuickbarSync();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("click", onTimetableEntryClick, true);
|
||||||
|
|
||||||
const handleTimetable = async () => {
|
const handleTimetable = async () => {
|
||||||
// Class entries (`div.entry.class`) load after the page shell; don't fail the whole
|
// Class entries (`div.entry.class`) load after the page shell; don't fail the whole
|
||||||
// setup if they are slow or briefly absent (e.g. navigation). Observers still catch them.
|
// setup if they are slow or briefly absent (e.g. navigation). Observers still catch them.
|
||||||
@@ -406,6 +509,7 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unregister();
|
unregister();
|
||||||
|
document.removeEventListener("click", onTimetableEntryClick, true);
|
||||||
observer?.disconnect();
|
observer?.disconnect();
|
||||||
quickbarObserver?.disconnect();
|
quickbarObserver?.disconnect();
|
||||||
if (quickbarSyncTimer !== null) clearTimeout(quickbarSyncTimer);
|
if (quickbarSyncTimer !== null) clearTimeout(quickbarSyncTimer);
|
||||||
|
|||||||
@@ -1,30 +1,8 @@
|
|||||||
/* Timetable Edit Plugin - BetterSEQTA Plus style */
|
/* Timetable Edit Plugin - BetterSEQTA Plus style */
|
||||||
|
|
||||||
/* Edit button in quickbar */
|
/* SEQTA sizes quickbar actions at 32×32 — layout comes from SEQTA + injected.scss */
|
||||||
.timetable-edit-quickbar-btn {
|
.timetablepage .quickbar .actions .timetable-edit-quickbar-btn {
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
background: transparent !important;
|
|
||||||
border: none !important;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timetable-edit-quickbar-btn:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.timetable-edit-quickbar-btn:active {
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
.timetable-edit-quickbar-btn svg {
|
|
||||||
fill: currentColor;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Edit modal animations */
|
/* Edit modal animations */
|
||||||
|
|||||||
+4
-1
@@ -6,6 +6,8 @@ import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
|||||||
|
|
||||||
// UI and theme management
|
// UI and theme management
|
||||||
import pageState from "@/pageState.js?url";
|
import pageState from "@/pageState.js?url";
|
||||||
|
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
|
||||||
|
import { installSeqtaMenuColourPatch } from "@/seqta/utils/patchSeqtaMenuUpdateColours";
|
||||||
|
|
||||||
// Stylesheets
|
// Stylesheets
|
||||||
import injectedCSS from "@/css/injected.scss?inline";
|
import injectedCSS from "@/css/injected.scss?inline";
|
||||||
@@ -15,6 +17,7 @@ export async function main() {
|
|||||||
try {
|
try {
|
||||||
if (settingsState.onoff) {
|
if (settingsState.onoff) {
|
||||||
injectPageState();
|
injectPageState();
|
||||||
|
installSeqtaMenuColourPatch();
|
||||||
|
|
||||||
// Rather permanent FIX for bug! -> this is a hack to get the injected.css file to have HMR in development mode as this import system is currently broken with crxjs
|
// Rather permanent FIX for bug! -> this is a hack to get the injected.css file to have HMR in development mode as this import system is currently broken with crxjs
|
||||||
if (import.meta.env.MODE === "development") {
|
if (import.meta.env.MODE === "development") {
|
||||||
@@ -35,6 +38,6 @@ export async function main() {
|
|||||||
|
|
||||||
function injectPageState() {
|
function injectPageState() {
|
||||||
const mainScript = document.createElement("script");
|
const mainScript = document.createElement("script");
|
||||||
mainScript.src = browser.runtime.getURL(pageState);
|
mainScript.src = resolveExtensionAssetUrl(pageState);
|
||||||
document.head.appendChild(mainScript);
|
document.head.appendChild(mainScript);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
|||||||
import { getAdaptiveColour } from "@/seqta/utils/adaptiveThemeColour";
|
import { getAdaptiveColour } from "@/seqta/utils/adaptiveThemeColour";
|
||||||
import { getCustomThemeAdaptiveCssVariableBindings } from "@/seqta/ui/colors/customThemeAdaptiveBindings";
|
import { getCustomThemeAdaptiveCssVariableBindings } from "@/seqta/ui/colors/customThemeAdaptiveBindings";
|
||||||
|
|
||||||
|
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
|
||||||
import darkLogo from "@/resources/icons/betterseqta-light-full.png";
|
import darkLogo from "@/resources/icons/betterseqta-light-full.png";
|
||||||
import lightLogo from "@/resources/icons/betterseqta-dark-full.png";
|
import lightLogo from "@/resources/icons/betterseqta-dark-full.png";
|
||||||
|
|
||||||
const ADAPTIVE_THEME_TRANSITION_MS = 400;
|
const ADAPTIVE_THEME_TRANSITION_MS = 400;
|
||||||
|
const LOGO_STYLE_ID = "bsplus-logo-style";
|
||||||
|
|
||||||
let colorTransitionRafId: number | null = null;
|
let colorTransitionRafId: number | null = null;
|
||||||
let lastInterpolatedHex: string | null = null;
|
let lastInterpolatedHex: string | null = null;
|
||||||
@@ -84,6 +86,23 @@ function cancelColorTransition() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Chromium does not always resolve extension URLs inside CSS variables on ::before. */
|
||||||
|
function applyBetterseqtaLogoBackground(isDark: boolean) {
|
||||||
|
const url = resolveExtensionAssetUrl(isDark ? darkLogo : lightLogo);
|
||||||
|
let styleEl = document.getElementById(LOGO_STYLE_ID);
|
||||||
|
if (!styleEl) {
|
||||||
|
styleEl = document.createElement("style");
|
||||||
|
styleEl.id = LOGO_STYLE_ID;
|
||||||
|
document.head.appendChild(styleEl);
|
||||||
|
}
|
||||||
|
styleEl.textContent = `
|
||||||
|
body.student #menu > ul::before,
|
||||||
|
#title::before {
|
||||||
|
background-image: url("${url}") !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function getRepresentativeRgbChannels(s: string): { r: number; g: number; b: number } | null {
|
function getRepresentativeRgbChannels(s: string): { r: number; g: number; b: number } | null {
|
||||||
const parsedHex = parseRepresentativeHex(s);
|
const parsedHex = parseRepresentativeHex(s);
|
||||||
if (!parsedHex) return null;
|
if (!parsedHex) return null;
|
||||||
@@ -115,11 +134,11 @@ function applyColorsWith(selectedColor: string) {
|
|||||||
let modeProps = {};
|
let modeProps = {};
|
||||||
modeProps = settingsState.DarkMode
|
modeProps = settingsState.DarkMode
|
||||||
? {
|
? {
|
||||||
"--betterseqta-logo": `url(${browser.runtime.getURL(darkLogo)})`,
|
"--betterseqta-logo": `url(${resolveExtensionAssetUrl(darkLogo)})`,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
"--better-pale": lightenAndPaleColor(selectedColor),
|
"--better-pale": lightenAndPaleColor(selectedColor),
|
||||||
"--betterseqta-logo": `url(${browser.runtime.getURL(lightLogo)})`,
|
"--betterseqta-logo": `url(${resolveExtensionAssetUrl(lightLogo)})`,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (settingsState.DarkMode) {
|
if (settingsState.DarkMode) {
|
||||||
@@ -129,6 +148,8 @@ function applyColorsWith(selectedColor: string) {
|
|||||||
document.documentElement.classList.remove("dark");
|
document.documentElement.classList.remove("dark");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyBetterseqtaLogoBackground(settingsState.DarkMode);
|
||||||
|
|
||||||
// Dynamic properties, always applied
|
// Dynamic properties, always applied
|
||||||
const rgbThreshold = GetThresholdOfColor(selectedColor);
|
const rgbThreshold = GetThresholdOfColor(selectedColor);
|
||||||
const isBright = rgbThreshold > 210;
|
const isBright = rgbThreshold > 210;
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ function buildFontOverrideCss(family: string): string {
|
|||||||
|
|
||||||
.iconFamily,
|
.iconFamily,
|
||||||
.iconFamily *,
|
.iconFamily *,
|
||||||
button.uiButton.timetable-zoom.iconFamily,
|
button.timetable-zoom.iconFamily,
|
||||||
|
button.bsplus-timetable-control.iconFamily,
|
||||||
[class~="iconFamily"],
|
[class~="iconFamily"],
|
||||||
[class~="iconFamily"] * {
|
[class~="iconFamily"] * {
|
||||||
font-family: "IconFamily" !important;
|
font-family: "IconFamily" !important;
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* SEQTA Learn bug (vanilla too): MainMenu.updateColours uses
|
||||||
|
* `.each(function (item) { this.options... }).bind(this)` — the bind is on
|
||||||
|
* `.each()`'s return value, not the callback. Saving a timetable subject colour
|
||||||
|
* sends `menu.update.colours` and throws.
|
||||||
|
*
|
||||||
|
* Also: ColourChooser (SlidePane + Modaliser) can leave a full-screen
|
||||||
|
* uiSlidePane / empty modaliser-container that blocks timetable clicks.
|
||||||
|
*
|
||||||
|
* Must run in the PAGE JavaScript context — inject via web_accessible script URL.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import browser from "webextension-polyfill";
|
||||||
|
import patchScript from "@/seqta/utils/seqtaMenuColourPatch.js?url";
|
||||||
|
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
|
||||||
|
|
||||||
|
const PAGE_PATCH_LOADER_ID = "bsplus-seqta-menu-colour-patch-loader";
|
||||||
|
|
||||||
|
/** Remove empty or hidden modaliser shells left after colour dialog teardown. */
|
||||||
|
export function dismissStaleModaliserContainers(): number {
|
||||||
|
let removed = 0;
|
||||||
|
for (const container of document.querySelectorAll(".modaliser-container")) {
|
||||||
|
const modal = container.querySelector(".modaliser");
|
||||||
|
const empty = !modal || modal.childElementCount === 0;
|
||||||
|
const hidden = !container.classList.contains("visible");
|
||||||
|
if (empty || hidden) {
|
||||||
|
container.remove();
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove stuck SEQTA colour chooser slide panes that intercept timetable clicks. */
|
||||||
|
export function dismissStaleColourSlidePanes(
|
||||||
|
forceColourChooser = false,
|
||||||
|
): number {
|
||||||
|
let removed = 0;
|
||||||
|
for (const pane of document.querySelectorAll(".uiSlidePane")) {
|
||||||
|
const isColourChooser = pane.querySelector(".pane.colourChooser");
|
||||||
|
if (isColourChooser) {
|
||||||
|
pane.remove();
|
||||||
|
removed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!forceColourChooser && pane.classList.contains("shown")) continue;
|
||||||
|
if (!pane.classList.contains("shown")) {
|
||||||
|
pane.remove();
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dismissStaleColourDialogs(forceColourChooser = false): {
|
||||||
|
slideRemoved: number;
|
||||||
|
modalRemoved: number;
|
||||||
|
} {
|
||||||
|
const slideRemoved = dismissStaleColourSlidePanes(forceColourChooser);
|
||||||
|
const modalRemoved = dismissStaleModaliserContainers();
|
||||||
|
document.body.classList.remove("clr-open");
|
||||||
|
document.documentElement.classList.remove("clr-open");
|
||||||
|
return { slideRemoved, modalRemoved };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function installSeqtaMenuColourPatch(): void {
|
||||||
|
if (document.getElementById(PAGE_PATCH_LOADER_ID)) return;
|
||||||
|
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.id = PAGE_PATCH_LOADER_ID;
|
||||||
|
script.src = resolveExtensionAssetUrl(patchScript);
|
||||||
|
script.addEventListener("load", () => script.remove());
|
||||||
|
(document.documentElement || document.head).appendChild(script);
|
||||||
|
}
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
/**
|
||||||
|
* PAGE context only — patches SEQTA menu.update.colours and removes stuck colour-dialog
|
||||||
|
* layers (uiSlidePane + modaliser) that block timetable entry clicks after a colour save.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
if (window.__bsplusMenuColoursPatched) return;
|
||||||
|
|
||||||
|
var LOG = "[BetterSEQTA+] timetable colour:";
|
||||||
|
var MENU_UPDATE_COLOURS = "menu.update.colours";
|
||||||
|
var SUBJECT_COLOUR_PREF_PREFIX = "timetable.subject.colour.";
|
||||||
|
var TUTOR_COLOUR_PREF_PREFIX = "timetable.tutor.";
|
||||||
|
|
||||||
|
function log(event, detail) {
|
||||||
|
if (detail !== undefined) {
|
||||||
|
console.info(LOG, event, detail);
|
||||||
|
} else {
|
||||||
|
console.info(LOG, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTesStylingEnabled() {
|
||||||
|
var logoStyle = document.getElementById("logo-style");
|
||||||
|
return logoStyle && logoStyle.textContent.indexOf("tesSeqta") !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function countOverlayState() {
|
||||||
|
return {
|
||||||
|
slidePanes: document.querySelectorAll(".uiSlidePane").length,
|
||||||
|
slidePanesShown: document.querySelectorAll(".uiSlidePane.shown").length,
|
||||||
|
colourChoosers: document.querySelectorAll(
|
||||||
|
".uiSlidePane .pane.colourChooser",
|
||||||
|
).length,
|
||||||
|
modalisers: document.querySelectorAll(".modaliser-container").length,
|
||||||
|
modalisersVisible: document.querySelectorAll(
|
||||||
|
".modaliser-container.visible",
|
||||||
|
).length,
|
||||||
|
quickbarsVisible: document.querySelectorAll(
|
||||||
|
".timetablepage .quickbar.visible",
|
||||||
|
).length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMenuSubjectColours() {
|
||||||
|
if (!window.user) return;
|
||||||
|
var defaultColour = isTesStylingEnabled() ? "#2b3547" : "#dddddd";
|
||||||
|
var items = document.querySelectorAll("#menu li[data-colour]");
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
var item = items[i];
|
||||||
|
var prefName = item.getAttribute("data-colour");
|
||||||
|
if (!prefName) continue;
|
||||||
|
var pref = window.user.getPreference(prefName);
|
||||||
|
var colour = (pref && pref.value) || defaultColour;
|
||||||
|
item.style.setProperty("--item-colour", colour);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissStaleModaliserContainers() {
|
||||||
|
var removed = 0;
|
||||||
|
var containers = document.querySelectorAll(".modaliser-container");
|
||||||
|
for (var i = 0; i < containers.length; i++) {
|
||||||
|
var container = containers[i];
|
||||||
|
var modal = container.querySelector(".modaliser");
|
||||||
|
var empty = !modal || modal.childElementCount === 0;
|
||||||
|
var hidden = !container.classList.contains("visible");
|
||||||
|
if (empty || hidden) {
|
||||||
|
container.remove();
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissStaleColourSlidePanes(forceColourChooser) {
|
||||||
|
var removed = 0;
|
||||||
|
var panes = document.querySelectorAll(".uiSlidePane");
|
||||||
|
for (var i = 0; i < panes.length; i++) {
|
||||||
|
var pane = panes[i];
|
||||||
|
var isColourChooser = pane.querySelector(".pane.colourChooser");
|
||||||
|
if (isColourChooser) {
|
||||||
|
pane.remove();
|
||||||
|
removed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!forceColourChooser && pane.classList.contains("shown")) continue;
|
||||||
|
if (!pane.classList.contains("shown")) {
|
||||||
|
pane.remove();
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissStaleColourDialogs(forceColourChooser) {
|
||||||
|
var slideRemoved = dismissStaleColourSlidePanes(forceColourChooser);
|
||||||
|
var modalRemoved = dismissStaleModaliserContainers();
|
||||||
|
document.body.classList.remove("clr-open");
|
||||||
|
document.documentElement.classList.remove("clr-open");
|
||||||
|
return {
|
||||||
|
slideRemoved: slideRemoved,
|
||||||
|
modalRemoved: modalRemoved,
|
||||||
|
overlays: countOverlayState(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function reconcileStuckQuickbars(reason) {
|
||||||
|
var fixed = 0;
|
||||||
|
var quickbars = document.querySelectorAll(".timetablepage .quickbar.visible");
|
||||||
|
for (var i = 0; i < quickbars.length; i++) {
|
||||||
|
var qb = quickbars[i];
|
||||||
|
var wrapper = qb.querySelector(".wrapper");
|
||||||
|
if (!wrapper || !wrapper.childElementCount) {
|
||||||
|
qb.classList.remove("visible");
|
||||||
|
fixed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fixed > 0) {
|
||||||
|
log("cleared stuck quickbar shell (" + reason + ")", { fixed: fixed });
|
||||||
|
try {
|
||||||
|
window.msg.send("calendar.quickbar.hide");
|
||||||
|
} catch (err) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findEntryElement(calendarId, code) {
|
||||||
|
if (calendarId) {
|
||||||
|
var byCalendar = document.querySelector(
|
||||||
|
".timetablepage .entry[data-calendarid=\"" + calendarId + "\"]",
|
||||||
|
);
|
||||||
|
if (byCalendar) return byCalendar;
|
||||||
|
}
|
||||||
|
if (code) {
|
||||||
|
var entries = document.querySelectorAll(".timetablepage .entry.class");
|
||||||
|
for (var i = 0; i < entries.length; i++) {
|
||||||
|
var entry = entries[i];
|
||||||
|
var titleEl = entry.querySelector(".title");
|
||||||
|
var title =
|
||||||
|
titleEl && titleEl.textContent ? titleEl.textContent.trim() : "";
|
||||||
|
if (title && title.indexOf(code) !== -1) return entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeQuickbarOpenContext(contents) {
|
||||||
|
if (!contents) return contents;
|
||||||
|
|
||||||
|
reconcileStuckQuickbars("before-open");
|
||||||
|
|
||||||
|
var element = contents.element;
|
||||||
|
var calendarId =
|
||||||
|
(element && element.getAttribute && element.getAttribute("data-calendarid")) ||
|
||||||
|
(contents.data && contents.data.calendarid);
|
||||||
|
var code = contents.data && contents.data.code;
|
||||||
|
var connected =
|
||||||
|
element &&
|
||||||
|
element.isConnected &&
|
||||||
|
typeof document.contains === "function" &&
|
||||||
|
document.contains(element);
|
||||||
|
|
||||||
|
if (!connected) {
|
||||||
|
var replacement = findEntryElement(calendarId, code);
|
||||||
|
if (replacement) {
|
||||||
|
contents.element = replacement;
|
||||||
|
log("replaced detached quickbar entry element", {
|
||||||
|
calendarId: calendarId,
|
||||||
|
code: code,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log("quickbar entry element detached, no replacement found", {
|
||||||
|
calendarId: calendarId,
|
||||||
|
code: code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logQuickbarOpenResult(phase) {
|
||||||
|
var qb = document.querySelector(".timetablepage .quickbar.visible");
|
||||||
|
var wrapper = qb && qb.querySelector(".wrapper");
|
||||||
|
log("quickbar open result (" + phase + ")", {
|
||||||
|
visible: !!qb,
|
||||||
|
hasWrapper: !!wrapper,
|
||||||
|
wrapperChildren: wrapper ? wrapper.childElementCount : 0,
|
||||||
|
overlays: countOverlayState(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleColourDialogCleanup(reason) {
|
||||||
|
var delays = [0, 100, 300, 600];
|
||||||
|
for (var i = 0; i < delays.length; i++) {
|
||||||
|
(function (delay) {
|
||||||
|
setTimeout(function () {
|
||||||
|
var result = dismissStaleColourDialogs(true);
|
||||||
|
if (
|
||||||
|
result.slideRemoved > 0 ||
|
||||||
|
result.modalRemoved > 0 ||
|
||||||
|
delay === 0
|
||||||
|
) {
|
||||||
|
log("cleanup (" + reason + ", +" + delay + "ms)", result);
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
})(delays[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSubjectOrTutorColourPref(handle) {
|
||||||
|
return (
|
||||||
|
typeof handle === "string" &&
|
||||||
|
(handle.indexOf(SUBJECT_COLOUR_PREF_PREFIX) === 0 ||
|
||||||
|
handle.indexOf(TUTOR_COLOUR_PREF_PREFIX) === 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function runMenuColourUpdate() {
|
||||||
|
log("menu.update.colours intercepted", countOverlayState());
|
||||||
|
try {
|
||||||
|
applyMenuSubjectColours();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[BetterSEQTA+] menu.update.colours failed:", err);
|
||||||
|
}
|
||||||
|
scheduleColourDialogCleanup("menu.update.colours");
|
||||||
|
reconcileStuckQuickbars("after-colour-save");
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixedMenuColourHandler() {
|
||||||
|
runMenuColourUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function neutralizeBrokenMenuColourListeners(msg) {
|
||||||
|
var listeners = msg.listeners && msg.listeners[MENU_UPDATE_COLOURS];
|
||||||
|
if (!listeners) return;
|
||||||
|
for (var i = 0; i < listeners.length; i++) {
|
||||||
|
if (listeners[i]) {
|
||||||
|
listeners[i].fn = fixedMenuColourHandler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchMsg(msg) {
|
||||||
|
if (!msg || msg.__bsplusPatched) return;
|
||||||
|
|
||||||
|
var originalSend = msg.send.bind(msg);
|
||||||
|
msg.send = function (handle, contents, suppressLogs, noRecord) {
|
||||||
|
if (handle === MENU_UPDATE_COLOURS) {
|
||||||
|
runMenuColourUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSubjectOrTutorColourPref(handle)) {
|
||||||
|
log("colour pref save detected", {
|
||||||
|
pref: handle,
|
||||||
|
colour: contents,
|
||||||
|
overlays: countOverlayState(),
|
||||||
|
});
|
||||||
|
var prefResult = originalSend(
|
||||||
|
handle,
|
||||||
|
contents,
|
||||||
|
suppressLogs,
|
||||||
|
noRecord,
|
||||||
|
);
|
||||||
|
scheduleColourDialogCleanup("pref:" + handle);
|
||||||
|
reconcileStuckQuickbars("after-colour-save");
|
||||||
|
return prefResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle === "calendar.quickbar.class") {
|
||||||
|
var openContext = normalizeQuickbarOpenContext(contents);
|
||||||
|
var label =
|
||||||
|
openContext &&
|
||||||
|
openContext.data &&
|
||||||
|
(openContext.data.description || openContext.data.code);
|
||||||
|
log("quickbar open msg.send", {
|
||||||
|
subject: label,
|
||||||
|
elementConnected:
|
||||||
|
openContext &&
|
||||||
|
openContext.element &&
|
||||||
|
openContext.element.isConnected,
|
||||||
|
overlays: countOverlayState(),
|
||||||
|
});
|
||||||
|
var openResult;
|
||||||
|
try {
|
||||||
|
openResult = originalSend(
|
||||||
|
handle,
|
||||||
|
openContext,
|
||||||
|
suppressLogs,
|
||||||
|
noRecord,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[BetterSEQTA+] quickbar open failed:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
setTimeout(function () {
|
||||||
|
logQuickbarOpenResult("+50ms");
|
||||||
|
}, 50);
|
||||||
|
setTimeout(function () {
|
||||||
|
logQuickbarOpenResult("+200ms");
|
||||||
|
}, 200);
|
||||||
|
return openResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle === "calendar.quickbar.hide") {
|
||||||
|
log("quickbar hide msg.send", countOverlayState());
|
||||||
|
return originalSend(handle, contents, suppressLogs, noRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalSend(handle, contents, suppressLogs, noRecord);
|
||||||
|
};
|
||||||
|
|
||||||
|
var originalRegister = msg.register.bind(msg);
|
||||||
|
msg.register = function (handle, callback, clear, ignoreHistory) {
|
||||||
|
if (handle === MENU_UPDATE_COLOURS) {
|
||||||
|
return originalRegister(
|
||||||
|
handle,
|
||||||
|
fixedMenuColourHandler,
|
||||||
|
clear,
|
||||||
|
ignoreHistory,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (handle === "calendar.quickbar.class") {
|
||||||
|
return originalRegister(
|
||||||
|
handle,
|
||||||
|
function (context) {
|
||||||
|
log("quickbar class handler invoked", {
|
||||||
|
elementConnected:
|
||||||
|
context &&
|
||||||
|
context.element &&
|
||||||
|
context.element.isConnected,
|
||||||
|
subject:
|
||||||
|
context &&
|
||||||
|
context.data &&
|
||||||
|
(context.data.description || context.data.code),
|
||||||
|
});
|
||||||
|
return callback(context);
|
||||||
|
},
|
||||||
|
clear,
|
||||||
|
ignoreHistory,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return originalRegister(handle, callback, clear, ignoreHistory);
|
||||||
|
};
|
||||||
|
|
||||||
|
neutralizeBrokenMenuColourListeners(msg);
|
||||||
|
msg.__bsplusPatched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryPatch() {
|
||||||
|
if (window.__bsplusMenuColoursPatched) return true;
|
||||||
|
if (!window.msg || !window.msg.send) return false;
|
||||||
|
patchMsg(window.msg);
|
||||||
|
window.__bsplusMenuColoursPatched = true;
|
||||||
|
log("patch active");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener(
|
||||||
|
"click",
|
||||||
|
function (event) {
|
||||||
|
var target = event.target;
|
||||||
|
if (!target || !target.closest) return;
|
||||||
|
var entry = target.closest(".timetablepage .entry");
|
||||||
|
if (!entry) return;
|
||||||
|
|
||||||
|
var before = countOverlayState();
|
||||||
|
var cleanup = dismissStaleColourDialogs(false);
|
||||||
|
var calendarId = entry.getAttribute("data-calendarid");
|
||||||
|
var instance = entry.getAttribute("data-instance");
|
||||||
|
var titleEl = entry.querySelector(".title");
|
||||||
|
var title = titleEl && titleEl.textContent ? titleEl.textContent.trim() : "";
|
||||||
|
log("entry click (capture)", {
|
||||||
|
calendarId: calendarId,
|
||||||
|
instance: instance,
|
||||||
|
title: title,
|
||||||
|
before: before,
|
||||||
|
cleanup: cleanup,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!tryPatch()) {
|
||||||
|
var interval = setInterval(function () {
|
||||||
|
if (tryPatch()) {
|
||||||
|
clearInterval(interval);
|
||||||
|
} else if (window.msg) {
|
||||||
|
neutralizeBrokenMenuColourListeners(window.msg);
|
||||||
|
}
|
||||||
|
}, 25);
|
||||||
|
setTimeout(function () {
|
||||||
|
clearInterval(interval);
|
||||||
|
}, 120000);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -1,12 +1,45 @@
|
|||||||
/**
|
/**
|
||||||
* SEQTA timetable uses Coloris for subject colours. Extension CSS previously
|
* SEQTA timetable colour picker (Coloris) recovery and click-blocker cleanup.
|
||||||
* unset Coloris ::after animations, which left the picker unable to reopen.
|
* Subject colour saves trigger SEQTA's broken menu.update.colours handler — see
|
||||||
* This module clears stuck `clr-open` / hidden picker state after each use.
|
* patchSeqtaMenuUpdateColours.ts.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let attached = false;
|
import { dismissStaleColourDialogs } from "@/seqta/utils/patchSeqtaMenuUpdateColours";
|
||||||
|
|
||||||
export function resetStuckColorisPicker(): void {
|
let attached = false;
|
||||||
|
let dismissTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
const DISMISS_DELAY_MS = 100;
|
||||||
|
|
||||||
|
function scheduleDismiss(): void {
|
||||||
|
if (dismissTimer !== null) clearTimeout(dismissTimer);
|
||||||
|
dismissTimer = setTimeout(() => {
|
||||||
|
dismissTimer = null;
|
||||||
|
dismissTimetableUiBlockers();
|
||||||
|
}, DISMISS_DELAY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hide colour-picker / modal layers that intercept clicks after a colour save. */
|
||||||
|
export function dismissTimetableUiBlockers(): {
|
||||||
|
slideRemoved: number;
|
||||||
|
modalRemoved: number;
|
||||||
|
} {
|
||||||
|
document.body.style.removeProperty("overflow");
|
||||||
|
|
||||||
|
for (const picker of document.querySelectorAll(".clr-picker")) {
|
||||||
|
picker.classList.remove("clr-open");
|
||||||
|
if (picker instanceof HTMLElement) {
|
||||||
|
picker.style.display = "none";
|
||||||
|
picker.style.pointerEvents = "none";
|
||||||
|
picker.style.visibility = "hidden";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dismissStaleColourDialogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clear inline styles that can prevent Coloris from reopening. */
|
||||||
|
export function prepareColorisPickerOpen(): void {
|
||||||
document.body.classList.remove("clr-open");
|
document.body.classList.remove("clr-open");
|
||||||
document.documentElement.classList.remove("clr-open");
|
document.documentElement.classList.remove("clr-open");
|
||||||
|
|
||||||
@@ -18,29 +51,44 @@ export function resetStuckColorisPicker(): void {
|
|||||||
picker.style.removeProperty("visibility");
|
picker.style.removeProperty("visibility");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const field of document.querySelectorAll(".clr-field")) {
|
|
||||||
field.classList.remove("clr-open");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function attachTimetableColorisRecovery(): void {
|
export function attachTimetableColorisRecovery(): void {
|
||||||
if (attached) return;
|
if (attached) return;
|
||||||
attached = true;
|
attached = true;
|
||||||
|
|
||||||
const afterColorisEvent = () => {
|
document.addEventListener("coloris:close", scheduleDismiss);
|
||||||
requestAnimationFrame(() => resetStuckColorisPicker());
|
document.addEventListener("coloris:pick", scheduleDismiss);
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("coloris:pick", afterColorisEvent);
|
|
||||||
document.addEventListener("coloris:close", afterColorisEvent);
|
|
||||||
|
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
"click",
|
"click",
|
||||||
(event) => {
|
(event) => {
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
if (!target.closest(".timetablepage [title='Choose a colour']")) return;
|
if (!target.closest(".timetablepage")) return;
|
||||||
resetStuckColorisPicker();
|
|
||||||
|
if (target.closest("[title='Choose a colour']")) {
|
||||||
|
if (dismissTimer !== null) {
|
||||||
|
clearTimeout(dismissTimer);
|
||||||
|
dismissTimer = null;
|
||||||
|
}
|
||||||
|
prepareColorisPickerOpen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target.closest(".entry")) return;
|
||||||
|
|
||||||
|
const pickerOpen =
|
||||||
|
document.body.classList.contains("clr-open") &&
|
||||||
|
document.querySelector(".clr-picker.clr-open");
|
||||||
|
if (!pickerOpen) {
|
||||||
|
const result = dismissTimetableUiBlockers();
|
||||||
|
if (result.slideRemoved > 0 || result.modalRemoved > 0) {
|
||||||
|
console.info(
|
||||||
|
"[BetterSEQTA+] timetable colour: content-script cleanup",
|
||||||
|
result,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -133,6 +133,13 @@ export default defineConfig(({ command }) => ({
|
|||||||
input: {
|
input: {
|
||||||
settings: join(__dirname, "src", "interface", "index.html"),
|
settings: join(__dirname, "src", "interface", "index.html"),
|
||||||
pageState: join(__dirname, "src", "pageState.js"),
|
pageState: join(__dirname, "src", "pageState.js"),
|
||||||
|
seqtaMenuColourPatch: join(
|
||||||
|
__dirname,
|
||||||
|
"src",
|
||||||
|
"seqta",
|
||||||
|
"utils",
|
||||||
|
"seqtaMenuColourPatch.js",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
assetFileNames: "assets/[name]-[hash][extname]",
|
assetFileNames: "assets/[name]-[hash][extname]",
|
||||||
|
|||||||
Reference in New Issue
Block a user