diff --git a/public/manifest.json b/public/manifest.json index d0b59e9..f9fbdf8 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -2,7 +2,7 @@ "name": "Canvas Strikethrough", "description": "Mark Canvas calendar events without deliverables as complete.", "author": "Daniel Stoiber", - "version": "1.5", + "version": "1.6", "manifest_version": 3, "permissions": [ "storage" diff --git a/src/content.ts b/src/content.ts index 6be7aac..26a9f0a 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1,37 +1,13 @@ -import { getEvents, getCalendarView } from "./getCalendarEvents"; -import { getCheckedCourses } from "./getCheckedCourses"; -import { getCheckedTodos } from "./storage"; -import { Todo, CalendarEvent } from "./types"; -import { checkEvent } from "./updateCalendarEvent"; import { displayCheckButton } from "./displayCheckButton"; -import { displayCheckedTodos } from "./displayCheckedTodos"; +import { injectCss } from "./injectCss"; +import { calendar } from "./updateCalendar"; -export async function calendar() { - - // Get checked courses - const courseList: number[] = getCheckedCourses(); - - // Get all checked assignments - const checkedTodos: Todo[] = getCheckedTodos(courseList); - - // Display checked todos list - displayCheckedTodos(); - - // Get all calendar events - const calendarView = getCalendarView(); - const calendarEvents: CalendarEvent[] = getEvents(calendarView); - - calendarEvents.forEach((event: CalendarEvent) => { - const { name, element } = event; - - // If assignment is checked - if (checkedTodos.find((todo: Todo) => todo.name.trim() === name.trim())) { - checkEvent(element); - } - }); -} +// Inject CSS +injectCss(); +// Get path const path = window.location.pathname; + if (path.includes('calendar')) { // Keep track of last time DOM was modified diff --git a/src/displayCheckButton.ts b/src/displayCheckButton.ts index f5cfd29..2dc08b1 100644 --- a/src/displayCheckButton.ts +++ b/src/displayCheckButton.ts @@ -1,9 +1,10 @@ import { getAssignment, getPage } from "./getTodos"; -import { markCompleteOnclick, markIncompleteOnclick } from "./markTodos"; +import { markCompleteOnclick, markIncompleteOnclick } from "./updateTodos"; import { isTodoChecked } from "./storage"; import { Todo } from "./types"; -function checkButtonHtml(courseId: number, todo: Todo, isChecked: boolean, isSubmitted: boolean): HTMLButtonElement { +function checkButtonHtml(todo: Todo, isChecked: boolean, isSubmitted: boolean): HTMLButtonElement { + const checkButton: HTMLButtonElement = document.createElement('button'); checkButton.innerHTML = isChecked ? 'Mark as incomplete' : 'Mark as complete'; checkButton.id = 'canvas-strikethrough-check-button'; @@ -11,11 +12,11 @@ function checkButtonHtml(courseId: number, todo: Todo, isChecked: boolean, isSub if (isChecked) { checkButton.onclick = () => { - markIncompleteOnclick(courseId, todo, checkButton, true); + markIncompleteOnclick(todo, checkButton, true); }; } else { checkButton.onclick = () => { - markCompleteOnclick(courseId, todo, checkButton, true); + markCompleteOnclick(todo, checkButton, true); }; } @@ -26,7 +27,7 @@ function checkButtonHtml(courseId: number, todo: Todo, isChecked: boolean, isSub function assignmentDisclaimer(): HTMLSpanElement { const disclaimer: HTMLSpanElement = document.createElement('span'); disclaimer.innerHTML = 'Note: Marking an assignment as complete is only for your own reference. It does not affect your submission/grade, nor does it notify your instructor.'; - disclaimer.style.fontStyle = 'italic'; + disclaimer.classList.add('disclaimer-text'); return disclaimer; } @@ -35,7 +36,7 @@ function assignmentDisclaimer(): HTMLSpanElement { function gradedDisclaimer(): HTMLSpanElement { const disclaimer: HTMLSpanElement = document.createElement('span'); disclaimer.innerHTML = 'Note: This assignment has already been submitted.'; - disclaimer.style.fontStyle = 'italic'; + disclaimer.classList.add('disclaimer-text'); disclaimer.style.color = 'var(--ic-link-color)'; return disclaimer; @@ -68,7 +69,7 @@ async function handleAssignment(courseId: number, assignmentId: number) { header.innerHTML = 'Canvas Strikethrough'; // Create check button - const checkButton = checkButtonHtml(courseId, todo, isChecked, isSubmitted); + const checkButton = checkButtonHtml(todo, isChecked, isSubmitted); buttonDiv.appendChild(header); buttonDiv.appendChild(checkButton); @@ -107,7 +108,7 @@ async function handlePage(courseId: number, pageUrl: string) { const isChecked = isTodoChecked(courseId, todo.id); // Create check button - const checkButton = checkButtonHtml(courseId, todo, isChecked, false); + const checkButton = checkButtonHtml(todo, isChecked, false); buttonDiv.appendChild(document.createElement('br')); buttonDiv.appendChild(checkButton); @@ -119,18 +120,31 @@ async function handleEvent(courseId: number, todoId: number | string, eventType: let todo: Todo | undefined = undefined; let isSubmitted = false; - if (typeof todoId === 'string') { + if (eventType === 'page') { const pageData = await getPage(courseId, todoId); + if (!pageData) { + return; + } + todo = pageData.todo; // Get check status isChecked = isTodoChecked(courseId, todo.id); - } else if (typeof todoId === 'number') { + } else if (eventType === 'assignment') { + + // Check if todoId is a number + if (typeof todoId !== 'number') { + return; + } const assignmentData = await getAssignment(courseId, todoId); + if (!assignmentData) { + return; + } + isSubmitted = assignmentData.isSubmitted todo = assignmentData.todo; @@ -144,7 +158,7 @@ async function handleEvent(courseId: number, todoId: number | string, eventType: return; } - const checkButton = checkButtonHtml(courseId, todo, isChecked, isSubmitted) + const checkButton = checkButtonHtml(todo, isChecked, isSubmitted) header.appendChild(checkButton); @@ -171,6 +185,10 @@ export async function displayCheckButton() { const courseId = parseInt(path.split('/')[2]); const assignmentId = parseInt(path.split('/')[4]); + if (Number.isNaN(courseId) || Number.isNaN(assignmentId)) { + return; + } + handleAssignment(courseId, assignmentId); return; } @@ -178,6 +196,11 @@ export async function displayCheckButton() { if (path.includes('pages')) { const courseId = parseInt(path.split('/')[2]); const pageUrl = path.split('/')[4]; + + if (Number.isNaN(courseId) || !pageUrl) { + return; + } + handlePage(courseId, pageUrl); return; } @@ -198,6 +221,7 @@ export async function displayCheckButton() { const url = eventUrl.split('/courses/')[1]; const courseId = parseInt(url.split('/')[0]); + console.log(url.split('/'), courseId); const eventType: 'assignment' | 'page' = url.split('/')[1] === 'assignments' ? 'assignment' : 'page'; let todoId: string | number = url.split('/')[2]; diff --git a/src/displayCheckedTodos.ts b/src/displayCheckedTodos.ts index 809bc5e..a591ca1 100644 --- a/src/displayCheckedTodos.ts +++ b/src/displayCheckedTodos.ts @@ -1,6 +1,6 @@ import { getCheckedCourses } from "./getCheckedCourses"; -import { markIncompleteOnclick } from "./markTodos"; -import { getCheckedTodos } from "./storage"; +import { markIncompleteOnclick } from "./updateTodos"; +import { getCheckedTodos, getCheckedTodosListState, toggleCheckedTodosListState } from "./storage"; import { Todo } from "./types"; export async function displayCheckedTodos() { @@ -25,20 +25,26 @@ export async function displayCheckedTodos() { const h2 = document.createElement('h2'); h2.tabIndex = -1; + // Get checked todos list state + const checkedTodoListOpen = getCheckedTodosListState(); + // Header button const span = document.createElement('span'); span.role = 'button'; span.id = 'checked-todos-button'; span.classList.add('element_toggler'); span.setAttribute('aria-controls', 'checked-todos'); - span.setAttribute('aria-expanded', 'false'); + span.setAttribute('aria-expanded', checkedTodoListOpen ? 'true' : 'false'); span.setAttribute('aria-label', 'Undated items toggle list visibility'); span.tabIndex = 0; + span.onclick = () => { + toggleCheckedTodosListState(); + } // Header button icon const i = document.createElement('i'); i.classList.add('auto_rotate'); - i.classList.add('icon-mini-arrow-right'); + i.classList.add(checkedTodoListOpen ? 'icon-mini-arrow-down' : 'icon-mini-arrow-right'); span.appendChild(i); span.appendChild(document.createTextNode(' Canvas Strikethrough Checked')); @@ -48,60 +54,45 @@ export async function displayCheckedTodos() { // List of checked todos const div = document.createElement('div'); div.id = 'checked-todos'; - div.style.display = 'none'; div.style.opacity = '1'; + // Hide list by default + div.style.display = checkedTodoListOpen ? 'block' : 'none'; const ul = document.createElement('ul'); - ul.style.listStyleType = 'none'; - ul.style.padding = '0'; - ul.style.margin = '0'; ul.id = 'checked-todos-list'; checkedTodos.forEach((todo, index) => { - const event = document.createElement('div'); - event.classList.add('checked-todo'); - event.classList.add('event'); - event.classList.add(`group_course_${todo.courseId}`); - event.id = `checked-todo-${index}`; - event.style.borderRadius = '3px'; - event.style.marginBottom = '3px'; - event.style.padding = '3px'; - event.style.color = 'white'; - event.setAttribute('data-course-id', `${todo.courseId}`); - event.setAttribute('data-todo-id', `${todo.id}`); - - const spanInner = document.createElement('a'); - spanInner.innerHTML = todo.name.length > 25 ? todo.name.substring(0, 25) + '...' : todo.name; - spanInner.classList.add('icon-calendar-month'); - spanInner.style.color = 'white'; - spanInner.style.textDecoration = 'none'; - spanInner.style.cursor = 'pointer'; + const todoParent = document.createElement('div'); + todoParent.classList.add('checked-todo'); + todoParent.classList.add('event'); + todoParent.classList.add(`group_course_${todo.courseId}`); + todoParent.id = `checked-todo-${index}`; + todoParent.setAttribute('data-course-id', `${todo.courseId}`); + todoParent.setAttribute('data-todo-id', `${todo.id}`); + + const aInner = document.createElement('a'); + aInner.innerHTML = todo.name; + aInner.classList.add('icon-calendar-month'); + aInner.href = `/courses/${todo.courseId}/${todo.type}s/${todo.id}`; const markAsIncompleteButton = document.createElement('button'); markAsIncompleteButton.classList.add('Button'); markAsIncompleteButton.classList.add('Button--link'); markAsIncompleteButton.innerText = 'Mark as incomplete'; - markAsIncompleteButton.style.padding = '3px'; - markAsIncompleteButton.style.margin = '3px'; - markAsIncompleteButton.style.backgroundColor = 'white'; - markAsIncompleteButton.style.color = 'black'; - markAsIncompleteButton.style.borderRadius = '3px'; - markAsIncompleteButton.style.cursor = 'pointer'; - markAsIncompleteButton.style.border = '1px solid black'; markAsIncompleteButton.onclick = () => { - markIncompleteOnclick(todo.courseId, todo, markAsIncompleteButton, true); + markIncompleteOnclick(todo, markAsIncompleteButton, true); // Remove checked todo from list const checkedTodo = document.querySelector(`#checked-todo-${index}`); checkedTodo?.remove(); }; - event.appendChild(spanInner); - event.appendChild(document.createElement('br')); - event.appendChild(markAsIncompleteButton); - ul.appendChild(event); + todoParent.appendChild(aInner); + // todoParent.appendChild(document.createElement('br')); + todoParent.appendChild(markAsIncompleteButton); + ul.appendChild(todoParent); }); div.appendChild(ul); diff --git a/src/getTodos.ts b/src/getTodos.ts index 1cccd93..cc66ed2 100644 --- a/src/getTodos.ts +++ b/src/getTodos.ts @@ -2,6 +2,10 @@ import { Todo } from "./types"; export async function getAssignment(courseId: number, assignmentId: number) { + if (!courseId || !assignmentId) { + return null; + } + // Get assignment data const assignmentReq = await fetch(`/api/v1/courses/${courseId}/assignments/${assignmentId}`); // Get submission data @@ -30,6 +34,10 @@ export async function getAssignment(courseId: number, assignmentId: number) { export async function getPage(courseId: number, pageId: number | string) { + if (!courseId || !pageId) { + return null; + } + // Check if pageId is a number or a string const pageIdType = typeof pageId === 'number' ? 'pageId' : 'pageUrl'; @@ -37,11 +45,13 @@ export async function getPage(courseId: number, pageId: number | string) { const pageReq = await fetch(`/api/v1/courses/${courseId}/pages/${pageId}`); const pageData = await pageReq.json(); + console.log(pageData); + // Create todo object const page: Todo = { type: 'page', courseId, - id: pageIdType === 'pageId' ? pageId : pageData.id, + id: typeof pageId === 'number' ? pageId : pageData.page_id, name: pageData.title }; diff --git a/src/injectCss.ts b/src/injectCss.ts new file mode 100644 index 0000000..9289305 --- /dev/null +++ b/src/injectCss.ts @@ -0,0 +1,59 @@ +export function injectCss() { + // Check if css is already injected + const existingCss = document.querySelector('#canvas-strikethrough-css'); + if (existingCss) { + return; + } + + // Create style element + const style = document.createElement('style'); + style.id = 'canvas-strikethrough-css'; + + // Add css + style.innerHTML = ` + #checked-todos-list { + list-style-type: none; + padding: 0; + margin: 0; + } + + .checked-todo { + border-radius: 3px; + margin-bottom: 3px; + padding: 3px; + color: white; + } + + .checked-todo a { + color: white; + text-decoration: none; + cursor: pointer; + text-decoration: none; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; + } + + .checked-todo a:hover { + text-decoration: underline; + } + + .checked-todo button { + padding: 3px; + margin: 3px; + background-color: white; + color: black; + border-radius: 3px; + cursor: pointer; + border: 1px solid black; + } + + .disclaimer-text { + font-style: italic; + } + `; + + // Append style to head + document.head.appendChild(style); +} \ No newline at end of file diff --git a/src/storage.ts b/src/storage.ts index 1a79d63..ed5c453 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -58,4 +58,27 @@ export function removeCheckedTodo(id: number) { // Save checked todos localStorage.setItem('checkedTodo', JSON.stringify(checkedTodos)); +} + +// Get checked todos list state +export function getCheckedTodosListState(): boolean { + const checkedTodosListState = localStorage.getItem('checkedTodoListState'); + + if (checkedTodosListState === 'true') { + return true; + } + + return false; +} + +// Set checked todos list state +export function setCheckedTodosListState(state: boolean) { + localStorage.setItem('checkedTodoListState', state.toString()); +} + +// Toggle checked todos list state +export function toggleCheckedTodosListState() { + const checkedTodosListState = getCheckedTodosListState(); + + setCheckedTodosListState(!checkedTodosListState); } \ No newline at end of file diff --git a/src/updateCalendar.ts b/src/updateCalendar.ts new file mode 100644 index 0000000..ddf6cb6 --- /dev/null +++ b/src/updateCalendar.ts @@ -0,0 +1,31 @@ +import { displayCheckedTodos } from "./displayCheckedTodos"; +import { getCalendarView, getEvents } from "./getCalendarEvents"; +import { getCheckedCourses } from "./getCheckedCourses"; +import { getCheckedTodos } from "./storage"; +import { Todo, CalendarEvent } from "./types"; +import { checkEvent } from "./updateCalendarEvent"; + +export async function calendar() { + + // Get checked courses + const courseList: number[] = getCheckedCourses(); + + // Get all checked assignments + const checkedTodos: Todo[] = getCheckedTodos(courseList); + + // Display checked todos list + displayCheckedTodos(); + + // Get all calendar events + const calendarView = getCalendarView(); + const calendarEvents: CalendarEvent[] = getEvents(calendarView); + + calendarEvents.forEach((event: CalendarEvent) => { + const { name, element } = event; + + // If assignment is checked + if (checkedTodos.find((todo: Todo) => todo.name.trim() === name.trim())) { + checkEvent(element); + } + }); +} diff --git a/src/markTodos.ts b/src/updateTodos.ts similarity index 75% rename from src/markTodos.ts rename to src/updateTodos.ts index 0fe8831..39f45b1 100644 --- a/src/markTodos.ts +++ b/src/updateTodos.ts @@ -4,7 +4,7 @@ import { addCheckedTodo, removeCheckedTodo } from "./storage"; import { CalendarEvent, Todo } from "./types"; import { checkEvent, uncheckEvent } from "./updateCalendarEvent"; -export function markIncompleteOnclick(courseId: number, todo: Todo, checkButton: HTMLButtonElement, updateCalendar: boolean = false) { +export function markIncompleteOnclick(todo: Todo, checkButton: HTMLButtonElement, updateCalendar: boolean = false) { // Remove checked assignment removeCheckedTodo(todo.id); checkButton.innerHTML = 'Mark as complete'; @@ -25,10 +25,10 @@ export function markIncompleteOnclick(courseId: number, todo: Todo, checkButton: }); } - checkButton.onclick = () => { markCompleteOnclick(courseId, todo, checkButton, updateCalendar) }; + checkButton.onclick = () => { markCompleteOnclick(todo, checkButton, updateCalendar) }; } -export function markCompleteOnclick(courseId: number, todo: Todo, checkButton: HTMLButtonElement, updateCalendar: boolean = false) { +export function markCompleteOnclick(todo: Todo, checkButton: HTMLButtonElement, updateCalendar: boolean = false) { // Add checked assignment addCheckedTodo(todo) checkButton.innerHTML = 'Mark as incomplete'; @@ -49,5 +49,5 @@ export function markCompleteOnclick(courseId: number, todo: Todo, checkButton: H }); } - checkButton.onclick = () => { markIncompleteOnclick(courseId, todo, checkButton, updateCalendar) }; + checkButton.onclick = () => { markIncompleteOnclick(todo, checkButton, updateCalendar) }; } \ No newline at end of file