0
\$\begingroup\$

My attempt at coding an adjusting calendar grid. It seems to work in most cases I've tried (although I haven't tested it exhaustively yet) but my DOM is a bit rusty and I wonder if there's better ways to do what I'm doing here.


class Session {
    day: number;
    hour: number;
    length: number;
    week: number;
    sem: number;
    module_code: string;
    module_name: string;
    room: string;
    staff: string[];
    cur_id: string | null;
    cur_layer: number | null; 
    constructor(day: number, sem: number, week: number, hour: number, length: number,
        module: string, name: string, room: string, staff: string[]) {
        this.day = day;
        this.hour = hour;
        this.length = length;
        this.week = week;
        this.sem = sem;
        this.module_code = module;
        this.module_name = name;
        this.room = room;
        this.staff = staff;
        this.cur_id = null;
        this.cur_layer = null;
    }
}

let dayNames = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"];

const WEEK_COUNT = 12;
const FIRST_HOUR = 9;
const LAST_HOUR = 20;

function idForSlot(day: number, sem: number, week: number, hour: number, layer: number): string {
    return "d" + day + "s" + sem + "w" + week + "h" + hour + "l" + layer;
}

function idForRowHeader(day: number, sem: number, hour: number) {
    return "d" + day + "s" + sem + "h" + hour + "rh";
}

function idForColumnHeader(day: number, sem: number, week: number) {
    return "d" + day + "s" + sem + "w" + week + "h";    
}

function elForSlot(day: number, sem: number, week: number, hour: number, layer: number): HTMLTableCellElement|null {
    return <HTMLTableCellElement|null>document.getElementById(idForSlot(day, sem, week, hour, layer));
}

function layerExists(day: number, sem: number, week: number, layer: number): boolean {
    return elForSlot(day, sem, week, FIRST_HOUR, layer) != null;
}


function createTTGrid(day: number, sem: number) {
    let body = document.getElementById("body")!;

    let tt_div = document.createElement("div");
    tt_div.id = "d" + day + "s" + sem;
    let tt_header = document.createElement("h2");
    tt_header.innerText = dayNames[day-1] + " Semester " + (sem);
    tt_div.append(tt_header);
    
    let table = document.createElement("table");
    table.id = "td" + day + "s" + sem;

    // Column headers
    let headerRow = document.createElement("tr");
    headerRow.append(document.createElement("th"));
    for (let i = 1; i <= WEEK_COUNT; i++) {
        let th = document.createElement("th");
        th.innerText = "Week " + i;
        th.id = idForColumnHeader(day, sem, i);
        headerRow.append(th);
        // Seperator
        let td = document.createElement("td");
        td.style.background = "black";
        td.style.width = "3px";
        td.style.minWidth = "3px";
        td.rowSpan = LAST_HOUR - FIRST_HOUR + 2;
        headerRow.append(td);
    }

    // Row headers and cells
    table.append(headerRow);
    for (let h=FIRST_HOUR; h<=LAST_HOUR; h++) {
        let row = document.createElement("tr");
        row.id = "d" + day + "s" + sem + "h" + h + "r";
        let th = document.createElement("th");
        th.innerText = h + "-" + (h+1);
        th.id = idForRowHeader(day, sem, h);
        row.append(th);
        for (let i=1; i<=WEEK_COUNT; i++) {
            let td = document.createElement("td");
            td.id = idForSlot(day, sem, i, h, 0);
            td.style.border = "1px solid black";
            row.append(td);
        }
        table.append(row);
    }
    tt_div.append(table);
    body.append(tt_div);
}

/**
 * Finds the predecessor element that the cell for a new layer should be inserted after.
 * This is
 *    a) the previous layer in the same week, if it exists
 *    b) the last layer in the previous week that exists, if there is one 
 *    c) if all else fails, the header cell for the row
 * @param day base-0 day number for new layer
 * @param sem base-1 semester number for new layer
 * @param week base-1 semester number for new layer
 * @param hour base-FIRST_HOUR hour number for new layer
 * @param layer intended number of new layer
 * @returns predecessor element
 */
function findNewLayerPredecessor(day: number, sem: number, week: number, hour: number, layer: number): HTMLElement {
    let prevlayerid = layer - 1;
    let prevlayerweek = week;
    let prevLayer = elForSlot(day, sem, prevlayerweek, hour, prevlayerid);

    while (!prevLayer && (prevlayerid > 0 || prevlayerweek > 0)) {
        prevlayerid--;
        if (prevlayerid < 0) {
            // Out of layers in this week, try previous week
            prevlayerweek--;
            let prevweekmaxlayer = 0;
            while (layerExists(day, sem, prevlayerweek, prevweekmaxlayer)) {
                prevweekmaxlayer++;
            }
            prevlayerid = prevweekmaxlayer - 1;
        }
        prevLayer = elForSlot(day, sem, prevlayerweek, hour, prevlayerid); 
    }

    if (prevLayer) {
        return prevLayer;
    } else {
        return document.getElementById(idForRowHeader(day, sem, hour))!;
    }

}

function addTTLayer(day: number, sem: number, week: number) {
    // Find top layer
    let freelayer = 0;
    while (layerExists(day, sem, week, freelayer)) { 
        freelayer++;
    }

    // Increase column span of header
    let colhead = <HTMLTableCellElement>document.getElementById(idForColumnHeader(day, sem, week));
    colhead.colSpan += 1;

    // Add layer
    for (let h=FIRST_HOUR; h<=LAST_HOUR; h++) {
        let newCell = document.createElement("td");
        newCell.id = idForSlot(day, sem, week, h, freelayer);
        newCell.style.border = "1px solid black";
        let prevLayer = findNewLayerPredecessor(day, sem, week, h, freelayer);
        prevLayer.after(newCell);
    }
}

function removeTTLayer(day: number, sem: number, week: number) {
    // Find top layer
    let lastlayer = 0;
    while (layerExists(day, sem, week, lastlayer)) {
        lastlayer++;
    }
    lastlayer--;
    if (lastlayer == 0) return;

    // Decrease column span of header
    let colhead = <HTMLTableCellElement>document.getElementById(idForColumnHeader(day, sem, week));
    colhead.colSpan -= 1;

    // Remove layer
    for (let h=FIRST_HOUR; h<=LAST_HOUR; h++) {
        let layerCell = elForSlot(day, sem, week, h, lastlayer); 
        layerCell?.remove();
    }
}

function addTTEvent(s: Session) {
    let day = s.day, sem = s.sem, week = s.week, hour = s.hour, len = s.length;

    if (s.cur_id) return;

    // Find free layer
    let freelayer = 0;
    let free = false;
    do {
        free = true;
        for (let i=0; i<len; i++) {
            let existingElement = elForSlot(day, sem, week, hour+i, freelayer);
            if (!existingElement) {
                free = false; // Presumably it was part of a prior event with rowspan
                break;
            }
            if (existingElement && existingElement.classList.contains("used")) {
                free = false;
                break;
            }
        }
        if (!free) {
            freelayer++;
            if (!layerExists(day, sem, week, freelayer)) {
                addTTLayer(day, sem, week);
                free = true;                
            }
        }
    } while (!free);

    // Remove empty blocks that will no longer be needed due to rowspan
    if (len > 1) {
        for (let i=1; i<len; i++) {
            let emptyBlock = elForSlot(day, sem, week, hour+i, freelayer);
            if (emptyBlock) emptyBlock.remove();
        }
    }
    
    // Add event
    let eventCell = document.createElement("td");
    eventCell.rowSpan = len;
    eventCell.classList.add("used");
    eventCell.style.border = "1px solid black";
    eventCell.style.backgroundColor = "lightblue";
    eventCell.innerHTML = s.module_code + "<br>" + s.module_name + "<br>" + s.room;
    let intendedId = idForSlot(day, sem, week, hour, freelayer);
    let oldCell = document.getElementById(intendedId)!;
    oldCell.replaceWith(eventCell);
    oldCell.remove();       
    eventCell.id = intendedId;
    
    s.cur_id = eventCell.id;
    s.cur_layer = freelayer;
}

function removeTTEvent(s: Session) {
    if (!s.cur_id) return;
    if (s.cur_layer == null) return; // Can't do !s.cur_layer because 0 is a valid layer

    
    // Remove event cell
    let eventCellId = s.cur_id;
    let eventCell = document.getElementById(eventCellId)!;
    let newEmptyCell = document.createElement("td");
    newEmptyCell.style.border = "1px solid black";
    eventCell.replaceWith(newEmptyCell);
    eventCell.remove();
    newEmptyCell.id = s.cur_id;
    s.cur_id = null; // Note, keep cur_layer around for empty layer check below
    
    // In case of rowspan, replace empty cells that rowspan previously covered
    if (s.length > 1) {
        for (let h=s.hour+1; h<=s.hour+s.length; h++) {
            let newCell = document.createElement("td");
            newCell.id = idForSlot(s.day, s.sem, s.week, h, s.cur_layer);
            newCell.style.border = "1px solid black";
            let prevLayer = findNewLayerPredecessor(s.day, s.sem, s.week, h, s.cur_layer);
            prevLayer.after(newCell);
        }
    }

    // check if layer became empty
    let layerEmpty = true;
    for (let h=FIRST_HOUR; h<=LAST_HOUR; h++) {
        let cell = elForSlot(s.day, s.sem, s.week, h, s.cur_layer);
        if (cell && cell.classList.contains("used")) {
            layerEmpty = false;
            break;
        }
    }

    // If layer become empty, remove it
    if (layerEmpty) {
        // Reduce header column span
        let colhead = <HTMLTableCellElement>document.getElementById("d" + s.day + "s" + s.sem + "w" + s.week + "h");
        colhead.colSpan -= 1;
    
        // Remove layer
        for (let h=FIRST_HOUR; h<=LAST_HOUR; h++) {
            let cell = elForSlot(s.day, s.sem, s.week, h, s.cur_layer);
            cell?.remove();
        }
        // Are there layers above?
        let highestLayerAbove = s.cur_layer + 1;
        while (layerExists(s.day, s.sem, s.week, highestLayerAbove)) {
            highestLayerAbove++;
        }
        // If yes, bring cells down a layer
        if (highestLayerAbove > s.cur_layer + 1) {
            for (let h=FIRST_HOUR; h<=LAST_HOUR; h++) {
                for (let l = s.cur_layer+1; l<highestLayerAbove; l++) {
                    let cell = elForSlot(s.day, s.sem, s.week, h, l);
                    if (cell) {
                        cell.id = idForSlot(s.day, s.sem, s.week, h, l-1);
                    }
                }
            }
        }
        
    }
    s.cur_layer = null;
}
    
function createAllTTGrids() {
    let body = document.getElementById("body");
    for (let sem=1; sem<3; sem++) {
        for (let day=1; day<6; day++) {
            createTTGrid(day, sem);
        }
    }

    for (let week=1; week<=WEEK_COUNT; week++) { 
        addTTEvent(new Session(1, 2, week, 13, 3, "COMP4005", "Introduction to Object Oriented Programming", "G204", ["Test"]));
        addTTEvent(new Session(1, 2, week, 13, 3, "COMP4005", "Introduction to Object Oriented Programming", "G202", ["Test"]));
        addTTEvent(new Session(1, 2, week, 13, 3, "COMP4005", "Introduction to Object Oriented Programming", "G102", ["Test"]));
        addTTEvent(new Session(1, 2, week, 13, 3, "COMP4005", "Introduction to Object Oriented Programming", "R237", ["Test"]));
    }

    let testsession = new Session(1, 1, 1, 9, 3, "CS2103T", "Software Engineering", "COM1-0210", ["Damith", "Wing"]);
    addTTEvent(testsession);
    let testsession2 = new Session(1, 1, 1, 10, 1, "CS2103T", "Software Engineering", "COM1-0210", ["Damith", "Wing"]);
    addTTEvent(testsession2);
    removeTTEvent(testsession);
}
````
\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.