mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-09-22 21:05:10 +02:00
Compare commits
3 Commits
5447891608
...
32321f4741
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32321f4741 | ||
|
|
88eb4fe033 | ||
|
|
7a3a73cfed |
@ -1,132 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
[INSERT CONTACT METHOD].
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
@ -63,6 +63,8 @@ Canvas Refined adds more with more to come!
|
||||
- Popup UI revamp
|
||||
- NEW Better todo list
|
||||
- Better sidebar
|
||||
- Grade analytics and history
|
||||
- Quiz safe mode
|
||||
- Global Canvas Search (search modules & assignments across all courses; Ctrl/Cmd+K)
|
||||
- Simplified UI
|
||||
- Some options won't show up when not needed
|
||||
@ -129,7 +131,9 @@ To install, run, and build with this repository locally,
|
||||
|
||||
### Add a new feature
|
||||
|
||||
To add a new feature, please follow these guidelines.
|
||||
To contribute to this, first either make an issue with feature suggestion or post in the discord.
|
||||
|
||||
If nobody is working on the feature, you can clone the dev branch and make your changes while following these guidelines.
|
||||
|
||||
#### Identifier
|
||||
|
||||
|
||||
153
js/content.js
153
js/content.js
@ -934,6 +934,8 @@ function applyOptionsChanges(changes) {
|
||||
case "custom_cards_3":
|
||||
moreAnnouncementCount = 0;
|
||||
moreAssignmentCount = 0;
|
||||
// A new timeframe starts back at the current window.
|
||||
betterTodoTimeframeOffset = 0;
|
||||
// loadBetterTodo();
|
||||
clearTodoList();
|
||||
createTodoSections(document.querySelector("#canvasrefined-todo-list"));
|
||||
@ -1898,19 +1900,78 @@ const BETTER_TODO_TIMEFRAME_DAYS = {
|
||||
"month": 30,
|
||||
"2month": 60,
|
||||
};
|
||||
// Look-ahead paging for the timeframe window (arrow buttons under the Tasks
|
||||
// header). 0 = the current window (now through the timeframe cutoff, with
|
||||
// overdue items kept). Each press of the right arrow advances one full
|
||||
// window further out, e.g. a 1-week timeframe on offset 1 shows only items
|
||||
// due in week 2 (the current week is skipped over).
|
||||
let betterTodoTimeframeOffset = 0;
|
||||
// null = show every class; a string courseId = only that class's tasks.
|
||||
let betterTodoProgressFilter = null;
|
||||
let domContainers = {};
|
||||
|
||||
// Resolve the persisted todo_timeframe option to a valid key ("all" when
|
||||
// unset/unknown).
|
||||
function getTodoTimeframeKey() {
|
||||
return (options.todo_timeframe && Object.prototype.hasOwnProperty.call(BETTER_TODO_TIMEFRAME_DAYS, options.todo_timeframe)) ? options.todo_timeframe : "all";
|
||||
}
|
||||
|
||||
// Better Todo timeframe filter, shared by the task list and the progress
|
||||
// display so their counts always agree: keeps items due on/before now+range
|
||||
// (overdue items are before now, so they are kept too). "all" keeps
|
||||
// everything.
|
||||
// display so their counts always agree: keeps items due inside the current
|
||||
// window. Offset 0 keeps everything on/before now+range (overdue items are
|
||||
// before now, so they are kept too); offset N>0 shows only the window
|
||||
// (now+N*range, now+(N+1)*range] so the current timeframe can be paged
|
||||
// through with the look-ahead arrows. "all" keeps everything.
|
||||
function applyTodoTimeframe(items) {
|
||||
betterTodoTimeframe = (options.todo_timeframe && Object.prototype.hasOwnProperty.call(BETTER_TODO_TIMEFRAME_DAYS, options.todo_timeframe)) ? options.todo_timeframe : "all";
|
||||
betterTodoTimeframe = getTodoTimeframeKey();
|
||||
if (betterTodoTimeframe === "all") return items;
|
||||
const cutoff = Date.now() + (BETTER_TODO_TIMEFRAME_DAYS[betterTodoTimeframe] * 24 * 60 * 60 * 1000);
|
||||
return items.filter(item => new Date(item.plannable_date).getTime() <= cutoff);
|
||||
const days = BETTER_TODO_TIMEFRAME_DAYS[betterTodoTimeframe];
|
||||
const now = Date.now();
|
||||
const cutoff = now + ((betterTodoTimeframeOffset + 1) * days * 24 * 60 * 60 * 1000);
|
||||
if (betterTodoTimeframeOffset <= 0) {
|
||||
return items.filter(item => new Date(item.plannable_date).getTime() <= cutoff);
|
||||
}
|
||||
const windowStart = now + (betterTodoTimeframeOffset * days * 24 * 60 * 60 * 1000);
|
||||
return items.filter(item => {
|
||||
const t = new Date(item.plannable_date).getTime();
|
||||
return t > windowStart && t <= cutoff;
|
||||
});
|
||||
}
|
||||
|
||||
// Date range for the timeframe pager under the Tasks header. The offset is
|
||||
// measured in full windows from now, so the dates shown are the exact slice
|
||||
// of items the list is currently filtered to.
|
||||
function getTodoTimeframeWindow() {
|
||||
const tf = getTodoTimeframeKey();
|
||||
const days = tf === "all" ? 0 : BETTER_TODO_TIMEFRAME_DAYS[tf];
|
||||
const offset = (tf === "all") ? 0 : betterTodoTimeframeOffset;
|
||||
const now = Date.now();
|
||||
const start = new Date(now + (offset * days * 24 * 60 * 60 * 1000));
|
||||
const end = new Date(now + ((offset + 1) * days * 24 * 60 * 60 * 1000));
|
||||
const fmt = (d) => d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
||||
return { tf, range: fmt(start) + " \u2013 " + fmt(end) };
|
||||
}
|
||||
|
||||
// Refresh the timeframe pager (arrows under the Tasks header) after every
|
||||
// render: update its label/date range, disable the back arrow on the
|
||||
// current window, and hide it entirely when there is no window to page
|
||||
// ("all") or when a non-Tasks tab is showing.
|
||||
function updateTodoTimeframeNav() {
|
||||
const nav = document.getElementById("better-todo-timeframe-nav");
|
||||
if (!nav) return;
|
||||
const tfWindow = getTodoTimeframeWindow();
|
||||
const paged = tfWindow.tf !== "all" && betterTodoFilter === "tasks";
|
||||
nav.style.display = paged ? "flex" : "none";
|
||||
if (!paged) return;
|
||||
const label = nav.querySelector("#better-todo-timeframe-label");
|
||||
if (label) label.textContent = tfWindow.range;
|
||||
const prev = nav.querySelector("#better-todo-timeframe-prev");
|
||||
if (prev) {
|
||||
const canPrev = betterTodoTimeframeOffset > 0;
|
||||
prev.disabled = !canPrev;
|
||||
prev.style.opacity = canPrev ? "1" : ".3";
|
||||
prev.style.cursor = canPrev ? "pointer" : "default";
|
||||
}
|
||||
}
|
||||
|
||||
// true when `courseId` is the dimmed-out class because another class is selected.
|
||||
@ -3086,6 +3147,47 @@ async function createTodoSections(location) {
|
||||
<h2 style="border:none !important;padding: 0">${todayString}</h2>
|
||||
`;
|
||||
|
||||
// Timeframe pager below the header: arrows page through the timeframe
|
||||
// window (e.g. a 1-week timeframe pages this week -> week 2 -> week 3,
|
||||
// skipping over everything before the window being shown). The left
|
||||
// arrow is disabled on the current window. Arrow keys also work while
|
||||
// hovering the pager.
|
||||
const timeframeNav = makeElement("div", location, { id: "better-todo-timeframe-nav" });
|
||||
timeframeNav.style = "display:none;align-items:center;justify-content:center;width:100%;margin-top:8px;user-select:none;";
|
||||
timeframeNav.innerHTML = `
|
||||
<button id="better-todo-timeframe-prev" type="button" title="Previous timeframe" style="display:flex;align-items:center;justify-content:center;width:22px;height:22px;padding:0;border:none;border-radius:50%;background:transparent;cursor:pointer;transition:all .2s ease;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 6L9 12L15 18" stroke="var(--bctext-0)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span id="better-todo-timeframe-label" style="font-size:12px;color:var(--bctext-0);white-space:nowrap;"></span>
|
||||
<button id="better-todo-timeframe-next" type="button" title="Look ahead to the next timeframe" style="display:flex;align-items:center;justify-content:center;width:22px;height:22px;padding:0;border:none;border-radius:50%;background:transparent;cursor:pointer;transition:all .2s ease;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 6L15 12L9 18" stroke="var(--bctext-0)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
const changeTimeframeOffset = (delta) => {
|
||||
const tf = getTodoTimeframeKey();
|
||||
if (tf === "all" || betterTodoFilter !== "tasks") return;
|
||||
const nextOffset = Math.max(0, betterTodoTimeframeOffset + delta);
|
||||
if (nextOffset === betterTodoTimeframeOffset) return;
|
||||
betterTodoTimeframeOffset = nextOffset;
|
||||
moreAssignmentCount = 0;
|
||||
clearTodoList();
|
||||
createTodoSections(location);
|
||||
};
|
||||
timeframeNav.querySelector("#better-todo-timeframe-prev").addEventListener("click", () => changeTimeframeOffset(-1));
|
||||
timeframeNav.querySelector("#better-todo-timeframe-next").addEventListener("click", () => changeTimeframeOffset(1));
|
||||
// Keyboard arrows page the timeframe while hovering the pager (and
|
||||
// only then, so normal arrow-key use elsewhere is untouched).
|
||||
const timeframeKeyHandler = (e) => {
|
||||
if (e.key === "ArrowRight") { e.preventDefault(); changeTimeframeOffset(1); }
|
||||
else if (e.key === "ArrowLeft") { e.preventDefault(); changeTimeframeOffset(-1); }
|
||||
};
|
||||
timeframeNav.addEventListener("mouseenter", () => document.addEventListener("keydown", timeframeKeyHandler));
|
||||
timeframeNav.addEventListener("mouseleave", () => document.removeEventListener("keydown", timeframeKeyHandler));
|
||||
|
||||
// placeholder for progress rings above the tab/filter control
|
||||
makeElement("div", location, { id: "better-todo-progress-placeholder", style: "display:flex;justify-content:center;margin-top:8px;" });
|
||||
|
||||
@ -3152,6 +3254,9 @@ async function createTodoSections(location) {
|
||||
mainSection.style = "display:flex;flex-direction:column;";
|
||||
}
|
||||
let mainSection = location.querySelector("#better-todo-main");
|
||||
// Refresh the timeframe pager's label/arrows on every render (visibility
|
||||
// depends on the current tab and the todo_timeframe option).
|
||||
updateTodoTimeframeNav();
|
||||
assignments.then(data => {
|
||||
const courseId = getCurrentCourseId();
|
||||
const scopedData = getTodoScopedData(data, courseId);
|
||||
@ -3570,7 +3675,12 @@ function populateAssignments(iscompleted = false) {
|
||||
? `<svg class="better-todo-assignment-edit" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:15px;height:15px;position:absolute;top:18px;right:5px;opacity:0.3;transition:all .3s ease;cursor:pointer;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.3'" title="Edit this custom task"><path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z" stroke="var(--bctext-0)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>`
|
||||
: "";
|
||||
const iconSize = isCustomTask ? 26 : 20;
|
||||
const iconLeftOffset = isCustomTask ? 2 : 5;
|
||||
// Optical centering inside the colored strip: the assignment icon's
|
||||
// glyph hugs its viewBox's left edge, so it gets a 5px nudge right.
|
||||
// The quiz glyph's visual weight already sits right of center (its
|
||||
// thin "motion lines" occupy the left edge), so it needs less of a
|
||||
// nudge — otherwise it reads as off-center next to the others.
|
||||
const iconLeftOffset = isCustomTask ? 2 : item.plannable_type == "quiz" ? 2 : 5;
|
||||
const taskIcon = removeIcons ? "" : isCustomTask
|
||||
? `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:100%;display:block;">
|
||||
<path d="M19.8201 14H15.6001C15.04 14 14.76 14 14.5461 14.109C14.3579 14.2049 14.2049 14.3578 14.1091 14.546C14.0001 14.7599 14.0001 15.0399 14.0001 15.6V19.82M20 12.7269V7.2C20 6.0799 20 5.51984 19.782 5.09202C19.5903 4.71569 19.2843 4.40973 18.908 4.21799C18.4802 4 17.9201 4 16.8 4H7.2C6.0799 4 5.51984 4 5.09202 4.21799C4.71569 4.40973 4.40973 4.71569 4.21799 5.09202C4 5.51984 4 6.0799 4 7.2V16.8C4 17.9201 4 18.4802 4.21799 18.908C4.40973 19.2843 4.71569 19.5903 5.09202 19.782C5.51984 20 6.0799 20 7.2 20H12.9496C13.4578 20 13.7118 20 13.9498 19.9407C14.1608 19.8882 14.3618 19.8016 14.5449 19.6844C14.7515 19.5522 14.926 19.3675 15.2751 18.9983L19.1254 14.9252C19.4486 14.5833 19.6101 14.4124 19.7255 14.2156C19.8278 14.041 19.903 13.8519 19.9486 13.6548C20 13.4325 20 13.1973 20 12.7269Z" stroke="var(--cr-todo-icon)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
@ -7041,6 +7151,7 @@ let gaCalc = null; // final-grade calculator settings for this course
|
||||
let gaCourseId = null; // course whose data is cached
|
||||
let gaData = null; // computed data for the current course
|
||||
let gaLoading = false;
|
||||
let gaChartObserver = null; // ResizeObserver: redraws charts once their boxes gain a real size
|
||||
|
||||
function gradeAnalyticsActive() {
|
||||
return options.grade_analytics === true && isGradesPage() && !quizSafeModeActive();
|
||||
@ -7143,6 +7254,7 @@ function syncGradeAnalyticsUI() {
|
||||
|
||||
function removeGradeAnalyticsPanel() {
|
||||
gaOpen = false;
|
||||
if (gaChartObserver) { gaChartObserver.disconnect(); gaChartObserver = null; }
|
||||
// Never leave a hypothetical Total or inline editors behind when the
|
||||
// panel goes away.
|
||||
gaClearImagineUI();
|
||||
@ -7328,6 +7440,19 @@ function ensureGradeAnalyticsPanel() {
|
||||
// the stored open state was restored), the earlier render call found no
|
||||
// panel — draw the charts now that it exists.
|
||||
if (gaOpen && gaData) renderGradeAnalytics();
|
||||
// A chart skipped because its box had no size (page still laying out,
|
||||
// tab/panel hidden) is left at width 0; this observer redraws the moment
|
||||
// the boxes get real dimensions, even when no DOM mutation or window
|
||||
// resize follows.
|
||||
if (gaChartObserver) gaChartObserver.disconnect();
|
||||
gaChartObserver = new ResizeObserver(() => {
|
||||
if (!gaOpen || !gaData) return;
|
||||
const pie = panel.querySelector("#canvasrefined-ga-pie");
|
||||
const line = panel.querySelector("#canvasrefined-ga-line");
|
||||
if ((pie && pie.width === 0) || (line && line.width === 0)) renderGradeAnalytics();
|
||||
});
|
||||
gaChartObserver.observe(panel.querySelector("#canvasrefined-ga-pie").parentNode);
|
||||
gaChartObserver.observe(panel.querySelector("#canvasrefined-ga-line").parentNode);
|
||||
return panel;
|
||||
}
|
||||
|
||||
@ -7858,9 +7983,17 @@ function renderGaHeatmap() {
|
||||
function gaSetupCanvas(canvas) {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const w = canvas.parentNode.clientWidth, h = canvas.parentNode.clientHeight;
|
||||
// Zero size means the panel isn't visible/attached yet, so skip drawing;
|
||||
// the DOM observer / resize handler redraws once it has real dimensions.
|
||||
if (w <= 0 || h <= 0) return null;
|
||||
// Zero size means the panel isn't visible/attached yet, so skip drawing —
|
||||
// but mark the canvas "never drawn" (width 0) so the self-heal paths
|
||||
// (syncGradeAnalyticsUI and the chart ResizeObserver) know to retry once
|
||||
// it has real dimensions. A canvas that was never drawn defaults to
|
||||
// width 300, so without this the zero-width "never drawn" check never
|
||||
// matches and charts stay blank until a tab switch or panel toggle.
|
||||
if (w <= 0 || h <= 0) {
|
||||
canvas.width = 0;
|
||||
canvas.height = 0;
|
||||
return null;
|
||||
}
|
||||
canvas.width = Math.max(1, Math.round(w * dpr));
|
||||
canvas.height = Math.max(1, Math.round(h * dpr));
|
||||
canvas.style.width = w + "px";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user