Update canvas on mouse move

The basic idea comes from the map of a game. According to the my code review, the map is a full-page canvas. I have no problem with drawing images on canvas. My question is how to detect map houses and update the canvas or even add click ability to it.
I have attached a GIF and HTML code from the original game to better understand my request.

enter image description here

<div id="canvasBorder"><canvas id="canvasMap"></canvas></div>

Okay, This is my code. It’s simple. I have drawn the map houses according to the main image which is large on the canvas.

function onClick2() {
  const imagePath = '/lobby/map.png';

  //Image Positions and Width/Height
  const array = [
    { x: 1764, y: 1104, w: 126, h: 84 },
    { x: 0, y: 1188, w: 126, h: 84 },
    { x: 126, y: 1188, w: 126, h: 84 },
    { x: 2090, y: 340, w: 126, h: 68 },
    { x: 126, y: 1188, w: 126, h: 84 },
  ];

  if (canvasRef?.current) {
    let x = canvasRef?.current.getContext('2d');

    let img = new Image();
    img.src = path;

    //Draw Map Blocks
    //Here I deleted the extra codes, I just wanted to show that it was done this way.
    if (x) {
      x.drawImage(
        img,
        array[3].x,
        array[3].y,
        array[3].w,
        array[3].h,
        0,
        0,
        array[3].w,
        array[3].h
      );
    }
  }
}

This is my result:
enter image description here

Here I need your guidance to understand the implementation trick. Here we need to recognize the mouse movement on the image or we need a number of squares that are rotated and have the image and work with the isPointInPath function.
If we proceed with the second way that I mentioned, to draw the squares, we need rotate(-0.25 * Math.PI);

Update canvas on mouse move

The basic idea comes from the map of a game. According to the my code review, the map is a full-page canvas. I have no problem with drawing images on canvas. My question is how to detect map houses and update the canvas or even add click ability to it.
I have attached a GIF and HTML code from the original game to better understand my request.

enter image description here

<div id="canvasBorder"><canvas id="canvasMap"></canvas></div>

Okay, This is my code. It’s simple. I have drawn the map houses according to the main image which is large on the canvas.

function onClick2() {
  const imagePath = '/lobby/map.png';

  //Image Positions and Width/Height
  const array = [
    { x: 1764, y: 1104, w: 126, h: 84 },
    { x: 0, y: 1188, w: 126, h: 84 },
    { x: 126, y: 1188, w: 126, h: 84 },
    { x: 2090, y: 340, w: 126, h: 68 },
    { x: 126, y: 1188, w: 126, h: 84 },
  ];

  if (canvasRef?.current) {
    let x = canvasRef?.current.getContext('2d');

    let img = new Image();
    img.src = path;

    //Draw Map Blocks
    //Here I deleted the extra codes, I just wanted to show that it was done this way.
    if (x) {
      x.drawImage(
        img,
        array[3].x,
        array[3].y,
        array[3].w,
        array[3].h,
        0,
        0,
        array[3].w,
        array[3].h
      );
    }
  }
}

This is my result:
enter image description here

Here I need your guidance to understand the implementation trick. Here we need to recognize the mouse movement on the image or we need a number of squares that are rotated and have the image and work with the isPointInPath function.
If we proceed with the second way that I mentioned, to draw the squares, we need rotate(-0.25 * Math.PI);

LoadingOverlay iterate many overlays and hide programmatically

I’m working with this libraray
https://gasparesganga.com/labs/jquery-loading-overlay/

Example: To show an overlay for an element:

$("body").LoadingOverlay("show", {});

Example: To hide an overlay for an element:

$("body").LoadingOverlay("hide", true);

I can call hide and show explicitly and it will work as expected.

I want to iterate over all overlays and hide them programmatically. What I tried doesn’t work.

I appreciate any assistance you can provide. Thank you as always!

`

$("body").LoadingOverlay("show", {});
$("#scotty").LoadingOverlay("show", {});

////These work when explicitly called
//$("body").LoadingOverlay("hide", true);
//$("#scotty").LoadingOverlay("hide", true);

//I want to interate over all overlays and hide them programatically. 
//This code will not hide the overlays.
$(".loadingoverlay").each(()=>{
    $(this).LoadingOverlay("hide", true);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/loadingoverlay.min.js"></script>

<img id="scotty" src="https://i.imgflip.com/1pg0vt.jpg" alt="" width="300">

`Hi, I have a list of nodes that

Load and process csv file in javascript

I am trying to load a csv file in javascript and filter it based on the values within it. My csv file was created with the following r script:

d <- structure(list(U = c("a", "b", "c", "d", "e", "f"), 
                    T = c(1, 2, 3, 4, 5, 6), 
                    aE = c("X", "Y", "Y", "Y", "Y", "Y"), 
                    aA = c("R", "S", "S", "T", "U", "V"), 
                    aX = c("P", "Q", "Q", "W", "P", "P"), 
                    aP = c("Z", "A", "A", "A", "Z", "A"), 
                    aJ = c("K", "L", "L", "K", "K", "L"), 
                    aD = c("C", "B", "B", "B", "D", "D"), 
                    bE = c("X", "Y", "Y", "X", "X", "Y"), 
                    bA = c("G", "R", "R", "I", "I", "T"), 
                    bX = c("M", "N", "N", "O", "M", "O"), 
                    bP = c("Z", "A", "A", "Z", "A", "Z"), 
                    bJ = c("K", "L", "L", "L", "K", "L"), 
                    bD = c("B", "C", "C", "C", "B", "C")), 
               row.names = c(NA, -6L), 
               class = c("tbl_df", "tbl", "data.frame"))

write.csv(d, "data.csv")

I then load it in javascript and attempt to process it using the following code:

$(document).ready(function() {
    var task = '1';
    var userID = 'a';

    fetch('https://raw.githubusercontent.com/username/repo/main/data.csv')
        .then(response => response.text())
        .then(csvData => {
            // Parse CSV data into an array of rows
            var rows = csvData.trim().split('n');

            // Extract headers and trim whitespace
            var headers = rows[0].split(',').map(header => header.trim());

            // Extract and parse each row (excluding headers)
            var data = rows.slice(1).map(row => {
                var rowValues = row.split(',').map(value => value.trim());
                var rowData = {};

                headers.forEach((header, index) => {
                    rowData[header] = rowValues[index];
                });

                return rowData;
            });

            console.log('Parsed Data:', data);

            // Filter data to only include rows that match the specified task and userID
            var filteredData = data.filter(row => row['T'] === task && row['U'] === userID);

            console.log('Filtered Data:', filteredData);

            // Display the filtered row in the HTML document
            if (filteredData.length > 0) {
                var resultRow = filteredData[0]; // Get the first (and only) matching row
                displayResultRow(resultRow); // Call function to display the row in the document
            } else {
                console.log('No matching row found.');
                // Handle case where no matching row is found
            }

            return filteredData; // Return the filtered array of data objects
        })
        .then(filteredData => {
            console.log('Returned Filtered Data:', filteredData);
            // Use the returned filtered array of data objects here or perform additional actions
        })
        .catch(error => {
            console.error('Error fetching or processing CSV:', error);
            // Handle fetch or processing errors
        });

    function displayResultRow(row) {
        // Assuming there is a container in your HTML document with id="resultContainer"
        var resultContainer = document.getElementById('resultContainer');
        
        // Clear previous content
        resultContainer.innerHTML = '';

        // Create and append elements to display the row data
        Object.keys(row).forEach(key => {
            var rowElement = document.createElement('div');
            rowElement.textContent = `${key}: ${row[key]}`;
            resultContainer.appendChild(rowElement);
        });
    }
});

The initial data loading works, as I am able to successfully see the data object. However none of the processing works successfully. I believe this is because the columns and rows may not be created correctly. This is how the data object looks in my js code editor:

Parsed Data:
(6) [{...}, {...}, {...}, {...}, {...}, ...]
0
:
(15) {"": ""1"", "U": ""1...}
1
:
(15) {"": ""2"", "U": ""2...}
2
:
(15) {"": ""3"", "U": ""3...}
3
:
(15) {"": ""4"", "U": ""4...}
4
:
(15) {"": ""5"", "U": ""5...}
5
:
(15) {"": ""6"", "U": ""6...}

This seems to me like the row numbers are being read in, and the column headers are treated as other values of some sort. In any case, the data filtering is not working. I also am unsure how to store the data object to access it.

How can I take the csv file that I successfully load into javascript, process it to select the only row that has U = the specified userID above, and T = the specified task above? How can I store this row as an object (say, entitled info_object) so that I can then extract the specific values of particular columns within that row (ex: save the value of aE as the object “info_aE”)?

json_encode (php) and json_parse (js) [closed]

I read the database with php and pack the contents into a Json:

$lbtb_json = json_encode($lbtbs);

In Javascript I get the values. When I with

this.lbtbdata = lbtb_json;    
window.console.log(this.lbtbdata);

Outputting the data, I get the following:
before parse

If I do the following in Javascript:

this.lbtbdata = lbtb_json;    
this.events = JSON.parse(this.lbtbdata);
window.console.log(this.events);

I get the following:

after parse

I would like to be able to output the following:
this.events[‘id’] or
this.events[‘date’] and so on.

Why don’t I get any more values after parsing? How do I get the values to be output individually, as described above?

Picklist values are not showing in the LWC Datatable

import { LightningElement,track,wire,api} from 'lwc';
import { updateRecord, deleteRecord} from 'lightning/uiRecordApi';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { refreshApex } from '@salesforce/apex';
import { getObjectInfo, getPicklistValues } from 'lightning/uiObjectInfoApi';
import OPPORTUNITYPRODUCT_OBJECT from '@salesforce/schema/OpportunityLineItem'; 
import DISCOUNT_TYPE from '@salesforce/schema/OpportunityLineItem.Discount_Type__c'; 

import getOpportunityLineItems from '@salesforce/apex/OpportunityLineItemController.getOpportunityLineItems';

const columns = [
{ label: 'ProductLineItemName', fieldName: 'ProductLineItemName__c'},
{ label:'Quantity', fieldName:'Quantity',editable:true},
{ label: 'Sales Price', fieldName: 'UnitPrice', editable:true},
{ label: 'Discount Value', fieldName: 'Discount_Value__c', editable:true},
{
    label: 'Discount Type',
    fieldName: 'Discount_Type__c',
    type: 'customPicklist',
    editable: true,
    typeAttributes: {
       
        options:{fieldName:'pickListOptions'},
        value: { fieldName: 'Discount_Type__c'},
        context:{ fieldName: 'Id'}
        
    }
},
{ label: 'GST %', fieldName: 'GST__c', editable:true},


{
    type: "button",
    typeAttributes: {
        iconName: "utility:delete",
        name: "deleteOpportunity",
        variant: "destructive",
        iconSize: "x-small",
        class: "small-button"
    }
}

];

export default class OpportunityLineItemsEditor extends LightningElement {

@api recordId;

opportunityLineData=[];
columns = columns;
draftValues=[];
oppLineProp;
discountTypeOptions=[];

@wire(getObjectInfo, { objectApiName: OPPORTUNITYPRODUCT_OBJECT })
    opportunityProductInfo;

    @wire(getPicklistValues, {
        recordTypeId: '$opportunityProductInfo.data.defaultRecordTypeId',
        fieldApiName: DISCOUNT_TYPE
    })
    wiredPicklistValues({ data, error }) {
        if (data) {
            this.discountTypeOptions = data.value;
        } else if (error) {
            console.error('Error fetching picklist values', error);
        }
    }


@wire(getOpportunityLineItems,{
    opportunityId:'$recordId',pickList:'$discountTypeOptions'
}) getOpportunityLine(result){
    this.oppLineProp=result;

    if(result.data)
    {
        this.opportunityLineData=JSON.parse(JSON.stringify(result.data));
        this.opportunityLineData=result.data.map((currItem) =>{
            let pickListOptions = this.discountTypeOptions;
            return {
                ...currItem,
                pickListOptions:pickListOptions
                
                //pickListOptions:picklistOptions
            }
        });

    }
    else if(result.error)
    {
        console.log('error while loading records');
    }


}

async handleSave(event)
{

    let records=event.detail.draftValues;
    let updateRecordsArray= records.map((currItem) => {
        let fieldInput = {...currItem};
        return{
            fields:fieldInput
        };

    });

    this.draftValues=[]
    let updateRecordsArrayPromise=updateRecordsArray.map((currItem) => updateRecord(currItem));

    await Promise.all(updateRecordsArrayPromise);

    const toastEvent = new ShowToastEvent({
        title: 'Success',
        message:
            'Records Updated Successfully',
            variant:'success'
    });
    console.log('Dispatching toast event...');
    this.dispatchEvent(toastEvent);
    console.log('Toast event dispatched.');
    await refreshApex(this.oppLineProp);

}

async handleRowAction(event) {
    if (event.detail.action.name === "deleteOpportunity") {
        deleteRecord(event.detail.row.Id).then(() => {
            
            this.dispatchEvent(
                new ShowToastEvent({
                    title: 'Success',
                    message: "Record deleted successfully!",
                    variant: 'success'
                })
            );
        }).catch((error) => {
            console.log("error, " + error);
            this.dispatchEvent(
                new ShowToastEvent({
                    title: 'Error deleting record',
                    message: error.body.message,
                    variant: 'error'
                })
            );
        })
    }

    await refreshApex(this.oppLineProp);
}

}

I have create a custom type Datatable here which extends standard LWC Datatable. Also, create an extra HTML file customPicklist.html, customPickListEdit.html so we can put the Picklist Combobox and its static value here. Below image of the structure

enter image description here

My code is working fine apart from picklist values so could you please tell me where i’m making mistake?

in our thesis, we have system should post the photo result of the detect if the detect_face funtion in the index.html

this is my code

# Define a global variable to track whether the video feed has finished
def detect_faces(username):
    global video_feed_finished

    video_capture = cv2.VideoCapture(2)  # Access the webcam (change to the appropriate device index if necessary)

    start_time = time.time()  # Record the start time
    while True:
        _, frame = video_capture.read()  # Read a frame from the webcam

        # Check if 5 seconds have elapsed
        if time.time() - start_time > 5:
            # Set the flag to indicate that the video feed is finished
            video_feed_finished = True
            # Stop processing frames after 5 seconds
            break

        # Perform face recognition using FaceNet model of DeepFace
        result = DeepFace.analyze(frame, detector_backend='mtcnn')

        # Insert the result into the MySQL database
        insert_result_into_database(username, result)

        calculate_age_difference(username)

        # Save the image to the database
        save_image_to_database(username, cv2.imencode('.jpg', frame)[1].tobytes())

        # Process the result as needed
        # For example, you can print the result to the console
        print(result)

        # Encode the analyzed frame as JPEG
        ret, jpeg = cv2.imencode('.jpg', frame)
        frame_bytes = jpeg.tobytes()

        # Yield the frame bytes as a response
        yield (b'--framern'
               b'Content-Type: image/jpegrnrn' + frame_bytes + b'rn')

    video_capture.release()

# Route for video feed here where it redirect to the index.html
@app.route('/video_feed')
def video_feed():
    global video_feed_finished

    # Check if the video feed is finished
    if video_feed_finished:
        # If finished, redirect to the login page
        return redirect(url_for('login'))

    # Start the face detection process
    return render_template('index.html')

@app.route('/video_feed_data', methods=['POST'])
def video_feed_data():
    if request.method == 'POST':
        username = request.form['username']
        return generate_video_feed(username)
    else:
        return "Method not allowed"
    
@app.route('/generate_video_feed')
def generate_video_feed(username):
    return Response(detect_faces(username), mimetype='multipart/x-mixed-replace; boundary=frame')

in this code the video feed go to the the index.html where the result in video feed data must be shown in the index.html which is the result photo, instead of doing that it post the photo result in separate page because of the mimetype it leave the index.html.

this one is from my html code

<body>
    <div class="art-container">
        <div class="art-shape" style="background: skyblue; width: 100px; height: 100px; top: 20%; left: 10%;"></div>
        <div class="art-shape" style="background: skyblue; width: 80px; height: 80px; top: 70%; left: 80%;"></div>
        <div class="art-shape" style="background: skyblue; width: 120px; height: 120px; top: 40%; left: 50%;"></div>
    </div>

    <h1>Real-time Face Recognition</h1>

    <form id="username-form" method="post" action="{{ url_for('video_feed_data') }}">
        <!-- Other form fields -->
        <label for="username">Username:</label>
        <input type="text" id="username" name="username" required>
        <!-- Submit button -->
        <button type="submit">Submit</button>
    
    <div class="video-container">
        <img id="video-feed" src="{{ url_for('video_feed_data') }}" alt="Video Feed">
    </div>

    <!-- Add a button that appears after the video feed is done -->
    <div id="refresh-button-container">
        <button onclick="refreshPage()">Log In</button>
    </div>

    <div id="error-message" style="display: none;">
        <p>Face could not be detected. Please refresh the page.</p>
    </div>


    
    <!-- Include the JavaScript file -->
    <script src="{{ url_for('static', filename='js/script.js') }}"></script>
</body>

and my JavaScript

// script.js

// JavaScript function to refresh the page
function refreshPage() {
    location.reload();
}

// Function to display the error message
function showError() {
    // Show the error message div
    document.getElementById('error-message').style.display = 'block';
}

// Function to hide the error message
function hideError() {
    // Hide the error message div
    document.getElementById('error-message').style.display = 'none';
}

// Function to handle face detection success or failure
function detectFaceFailure() {
    // Check if there was an error loading the video feed
    const videoFeedError = document.getElementById('video-feed').naturalWidth === 0;

    if (videoFeedError) {
        // Show the error message if there was an error loading the video feed
        showError();
    } else {
        // Hide the error message if face detection was successful
        hideError();
    }
}

// Call the detectFaceFailure() function when the page loads (or at the appropriate time)
window.onload = detectFaceFailure;

// Check if the video feed has loaded successfully
document.getElementById('video-feed').onload = () => {
    console.log('Video feed loaded');
    // Show the refresh button
    document.getElementById('refresh-button-container').style.display = 'block';
};

// Handle error if face detection fails
document.getElementById('video-feed').onerror = () => {
    console.error('Error loading video feed');
    // Show the error message
    document.getElementById('error-message').style.display = 'block';
};

function toggleRefreshButton() {
    const username = document.getElementById('username').value;
    const refreshButtonContainer = document.getElementById('refresh-button-container');
    const errorMessage = document.getElementById('error-message');

    // Check if username is not empty
    if (username.trim() !== '') {
        refreshButtonContainer.style.display = 'block';
        errorMessage.style.display = 'none'; // Hide error message if shown
    } else {
        refreshButtonContainer.style.display = 'none';
    }
}

function refreshPage() {
    location.reload();
}


Racaptcha value empty [closed]

I have spent more than two days trying to solve this issue, but I still can’t get the reCAPTCHA value to return properly in my form. Interestingly, when I log the field using console.log, it does return the value. Additionally, it’s worth noting that the form spans across two pages

provide the solution

Trying to generate unique match pairing for each team similar to 2024/25 UEFA Champion League Swiss system format

I’m trying to make a tournament pairing similar to the new UEFA Champion League format. There are 36 teams competing in the league phase. They are divided into 4 pots. Each team will play 8 matches (2 opponents from each pot). That adds up to 144 matches in total. I have a hard time coming up a function with good backtracking logic. Here is my failed code:

https://jsfiddle.net/4cgm1Lo8/5/

const pots = [
    ["A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8"],
    ["B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8"],
    ["C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8"],
    ["D1", "D2", "D3", "D4", "D5", "D6", "D7", "D8"],
];

const MATCH_COUNT = 144;
const MAX_ROUNDS = 8

function run() {
    const teamIds = _.flatten(pots)
    return teamIds.reduce((matches,thisTeamId) => {
        for (let round = 0; round < MAX_ROUNDS; round++) {
            const thisTeamMatches = matches.filter(match => match.includes(thisTeamId))
            if (thisTeamMatches.length >= MAX_ROUNDS) {
                break;
            }
            const pool = teamIds.filter(poolTeamId => {
                const encounteredBefore = thisTeamMatches.find(match => match.includes(poolTeamId))
                const potEncounterCount = thisTeamMatches.filter(match => {
                    const opponentId = match.find(m => m != thisTeamId)
                    return getTeamPot(opponentId, pots) === getTeamPot(poolTeamId, pots)
                })
                const poolTeamIdMatches = matches.filter(match => match.includes(poolTeamId))
                return poolTeamId != thisTeamId && !encounteredBefore && potEncounterCount.length < 2 && poolTeamIdMatches.length < MAX_ROUNDS
            })
            matches.push([thisTeamId, _.sample(pool)])
        }
        return matches
    }, [] as string[][])
}



function getTeamPot(teamId: string, pots: string[][]) {
    return pots.findIndex((pot) =>
        pot.find((potTeamId) => potTeamId === teamId),
    );
}

function getOpponent(yourTeamId: string, match: string[][]){
    return match.find(m => m != thisTeamId)                 
}

console.log(run())

This function is unable to create 144 matches. Some of the matches created have undefined opponents. The code really doesn’t account for situations where it is unable to find a valid opponent for a team.

How would you make a backtracking method so that it can fill up some of the matchups that contains undefined opponent?

my initialization of swiper js with npm, the starting script does not work

I’m starting with npm and despite several tutorials, advice, I can’t get the swipe JS library to work, only js, not with a framework

Link : https://swiperjs.com/get-started

I created a file for my script, I took the basic code to initialize my slider:
script.js

// Swiper
import {Swiper} from '../../node_modules/swiper/swiper-bundle.js';

const swiper = new Swiper('.swiper', {
  // Optional parameters
  direction: 'vertical',
  loop: true,

  // If we need pagination
  pagination: {
    el: '.swiper-pagination',
  },

  // Navigation arrows
  navigation: {
    nextEl: '.swiper-button-next',
    prevEl: '.swiper-button-prev',
  },

  // And if we need scrollbar
  scrollbar: {
    el: '.swiper-scrollbar',
  },
});

HTML, I added a module type because the console asked me to do so

<script type="module"  src="dist/js/script.js"></script>
</body>
</html> 

css /scss -> here ok
@use '../../node_modules/swiper/swiper-bundle.css';

Netsuite : AddFilter(position, reference, type, label, displayType, defaultValue, helpText, source, maxLength) Please enter valid default value error

I’m trying to customise the suitelet of the EFT Bill Payment and set a defaultValue on a filter.

I found this documentation : [https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_0724095855.html]

here is the code that I try to implement :

const onRequest = (context) => {
try {

            var epPlugin = plugin.loadImplementation({type: "customscript_17239_ep_api_plugin",implementation: "default"});
            var epPaymentSelectionForm = epPlugin.getEPForm(); epPaymentSelectionForm.setPaymentType('EFT');
            epPaymentSelectionForm.setGlobalPayment(false);
            
            //FILTERS 
            epPaymentSelectionForm.AddFilter(true,'custbody18', 'checkbox', 'Block for payment', 'normal', 'T');
            epPaymentSelectionForm.AddFilter(true,'custbody17', 'multiselect', 'Payment Method', 'inline','1,6', '', 'customlist819', '');
        
          
            //COLUMNS
            epPaymentSelectionForm.AddColumn('text', 'Payment Method', 'custbody17'); 
            epPaymentSelectionForm.BuildUI(context);
            var form = epPaymentSelectionForm.GetForm();
            context.response.writePage(form);
            
         } catch (e) {
            log.error("error in loading",e)
        }
    }

    return {onRequest}

});

the error is the following : ‘Invalid value. Please enter valid default value.’ I have already try ‘T’, ‘true’, ‘True’, true none of them work.

Anyone has ever faced this issue ?

Thank you,

The defaultValue is never set neither for the first filter or the second one.

Codemirror customize JavaScript linter

I’m using CodeMirror 6 with Angular 14.
I’d like to turn off some parsing errors I get with this linter.

This is my code:

// other import...
import { basicSetup } from 'codemirror';
import { javascript, esLint } from '@codemirror/lang-javascript';
import { EditorState } from '@codemirror/state';
import { EditorView, keymap } from '@codemirror/view';
import { indentWithTab, redo, undo } from '@codemirror/commands';
import { linter, lintGutter } from '@codemirror/lint';
import * as eslint from "eslint-linter-browserify";

@ViewChild('textEditor', { static: false }) textEditor: any;
editorView: EditorView;
val: string = '';

// constructor, other functions...
initCodeMirror() {
    let editorElement = this.textEditor.nativeElement;
    let context = this;

    textMode = javascript();
    const config = {
        parserOptions: {
        ecmaVersion: 6,
    }
    let exts = [basicSetup, keymap.of([indentWithTab])];
    exts.push(lintGutter(), linter(esLint(new eslint.Linter(), config)));
    let state: EditorState = EditorState.create({
        doc: this.val,
        extensions: exts
    });
    this.editorView = new EditorView({
        state,
        parent: editorElement
    });

    this.editorView.focus();
}

I found this example but here it explains how to create a completely custom linter and that’s not what I want.

I would like to know if there is a way to create a custom linter starting from a set of rules defined in a standard linter (like the one I’m using).

Total times in a dynamic table is not calculating correctly

Good afternoon,
I have a simple HTML page that run batch stopwatches, and you can log athlete’s bib numbers accordingly to log split times. However, my total time is calculated wrong, as it add to the current time every time. I am not an expert in coding, so need some help finding the issue and fix it. WIll really appreciate if someone can help me

Here is my code:

<!DOCTYPE html>
<html lang="en">
<head>
 
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=0.8">
  <title>XCO Timer</title>
  <style>
    .stopwatch {
      text-align: center;
      margin-bottom: 20px;
    }
    table {
   
      border-collapse: collapse;
    }
    th, td {
      border: 1px solid black;
      padding: 8px;
      text-align: left;
    }
        .stopwatch {
          <!-- font-family: Arial, Helvetica, sans-serif; -->
          border-collapse: collapse;
          width: 100%;
        }

        .stopwatch td, .stopwatch th {
          border: 1px solid #ddd;
          padding: 8px;
        }

        .stopwatch tr:nth-child(even){background-color: #f2f2f2;}

        .stopwatch tr:hover {background-color: #ddd;}

        .stopwatch th {
          padding-top: 12px;
          padding-bottom: 12px;
          text-align: left;
          background-color: #04AA6D;
          color: white;
        }
    .stopwatch th:nth-child(1) {
      width: 50px;
    }

    .stopwatch th:nth-child(2) {
      width: 200px;
    }

    .stopwatch th:nth-child(n+3) {
      width: 80px;
    }
    
    #userInput {
      text-align: center;
      margin-bottom: 20px;
    }
    #userInput input[type="text"], #userInput button {
      margin: 5px;
    }
    #userInput input[type="text"] {
      padding: 5px;
      border: 1px solid #ccc;
      border-radius: 3px;
    }

    .green-button {
      background-color: #04AA6D;
      color: white;
      border: none;
      padding: 10px 20px;
      text-align: center;
      text-decoration: none;
      display: inline-block;
      font-size: 16px;
      margin: 2px 2px;
      cursor: pointer;
      border-radius: 2px;
    }

    .blue-button {
      background-color: #6495ED;
      color: white;
      border: none;
      padding: 10px 20px;
      text-align: center;
      text-decoration: none;
      display: inline-block;
      font-size: 16px;
      margin: 2px 2px;
      cursor: pointer;
      border-radius: 2px;
    }   
    
        .red-button {
      background-color: #E97451;
      color: white;
      border: none;
      padding: 10px 20px;
      text-align: center;
      text-decoration: none;
      display: inline-block;
      font-size: 16px;
      margin: 2px 2px;
      cursor: pointer;
      border-radius: 2px;
    }   
    
    #athleteTable {
  border-collapse: collapse;
  width: 100%;
}

#athleteTable th, #athleteTable td {
  border: 1px solid #ddd;
  padding: 8px;
}

#athleteTable tbody tr:nth-child(even) {
  background-color: #f2f2f2;
}

#athleteTable tbody tr:hover {
  background-color: #ddd;
}

#athleteTable th {
  padding-top: 12px;
  padding-bottom: 12px;
  text-align: left;
  background-color: #04AA6D;
  color: white;
}

#athleteTable th:nth-child(1) {
  width: 30%; /* Adjust width as needed */
}

#athleteTable th:nth-child(n+2) {
  width: 10%; /* Adjust width as needed */
}

#timerA,
#timer${batch} {
  font-weight: bold;
  font-size: 24px; /* Adjust font size as needed */
}

#timerContainer {
  font-weight: bold;
  font-size: 24px; /* Adjust font size as needed */

  display: inline-block; /* Ensure the container only takes up as much space as needed */
}

p.solid {border-style: solid;}


#userInput input[type="text"] {
  padding: 10px 20px; /* Adjust padding as needed */
  border: none;
  background-color: #f2f2f2; /* Match the button's background color */
  color: black; /* Match the button's text color */
  font-size: 16px; /* Match the button's font size */
  margin: 5px; /* Match the button's margin */
  cursor: pointer;
  border-radius: 2px; /* Match the button's border radius */
      width: 100px;
}

  </style>
</head>
<body>
  <div id="userInput">
    <input type="text" id="bibNumber" inputmode="numeric" pattern="d*" placeholder="Enter Bib No" title="Please enter numeric value only" onkeyup="handleKeyPress(event)">
    <button class="green-button" onclick="recordBibNumber()">Enter</button>
  </div>

   <div id="stopwatches">
    <!-- Batch A Stopwatch -->
    <div class="stopwatch" id="batchA">
      <h2>Batch A Stopwatch</h2>
  <div id="timerContainer">
          <p class="solid" id="timerA">00:00:00.00</p>
        </div><br>
      <button class="green-button" onclick="startTimer('A')">Start</button>
      <button class="green-button" onclick="stopTimer('A')">Stop</button>
      <button class="green-button" onclick="resetTimer('A')">Reset</button>
      <table id="tableA">
        <thead>
          <tr>
            <th>Bib No</th>
            <th>Name</th>
           <th>Total Laps</th>
           <th>Total Time</th>                 
            <!-- Dynamic lap headers -->
            <th id="lapHeadersA"></th>
          </tr>
        </thead>
        <tbody id="tbodyA"></tbody>
      </table>
    </div>
  </div>
  
  
  
  
  <button class="blue-button" onclick="startNewBatch()">Add New Batch</button>

  <!-- Form for adding athlete information -->
  <h2>Add Athlete Information</h2>
  
  
 <form id="athleteForm">
  <label for="name">Name:</label>
  <input type="text" id="name" name="name" required autocomplete="off"><br><br>
  
  <label for="bibNo">Bib No:</label>
  <input type="text" id="bibNoInput" name="bibNo" pattern="d*" title="Please enter numeric value only" required autocomplete="off"><br><br>
  
  <label for="batch">Batch:</label>
  <select id="batch" name="batch" required autocomplete="off">
    <option value="">Select batch</option>
    <!-- Add options for batch A to Z -->
    <script>
      for (let i = 65; i <= 90; i++) {
        let letter = String.fromCharCode(i);
        document.write(`<option value="${letter}">${letter}</option>`);
      }
    </script>
  </select><br><br>
  
  <label for="gender">Gender:</label>
  <select id="gender" name="gender" required autocomplete="off">
    <option value="Male">Male</option>
    <option value="Female">Female</option>
  </select><br><br>
  
  <label for="ageCategory">Age Category:</label>
  <select id="ageCategory" name="ageCategory" required autocomplete="off">
    <option value="Nipper">Nipper (8-10 years old)</option>
    <option value="Sprog">Sprog (11-12 years old)</option>
    <option value="Youth">Youth (13-14 years old)</option>
    <option value="Junior">Junior (15-16 years old)</option>
    <option value="Under23">Under 23 (17-22 years old)</option>
    <option value="Elite">Elite (23-29 years old)</option>
    <option value="SubVeteran">Sub-Veteran (30-39 years old)</option>
    <option value="Veteran">Veteran (40-49 years old)</option>
    <option value="Master">Master (50-59 years old)</option>
    <option value="GrandMaster">Grand Master (60-69 years old)</option>
    <option value="GreatGrandMaster">Great Grand Master (70+ years old)</option>
  </select><br><br>
  
  <button class="green-button" type="submit">Add Athlete</button>
    <button class="red-button" onclick="clearTable()">Clear Table</button>
</form>

  <!-- Table to display athlete information -->
  <h2>Athlete Information</h2>
  <table id="athleteTable">
    <thead>
      <tr>
        <th>Name</th>
        <th>Bib No</th>
        <th>Batch</th>
        <th>Gender</th>
        <th>Age Category</th>
        <th>Edit</th>
      </tr>
    </thead>
    <tbody id="athleteTableBody"></tbody>
  </table>

  <script>
  
   let athletes = [];

  // Add default athletes for testing
  athletes.push({ name: "John", bibNo: "1", batch: "A", gender: "Male", ageCategory: "Sprogs" });
  athletes.push({ name: "Peter", bibNo: "2", batch: "B", gender: "Male", ageCategory: "Sprogs" });
  athletes.push({ name: "Gids", bibNo: "3", batch: "B", gender: "Male", ageCategory: "Sprogs" });
  athletes.push({ name: "Mack", bibNo: "4", batch: "C", gender: "Male", ageCategory: "Sprogs" });

  let editIndex = -1;

  // Render athlete table after adding default athletes
  renderAthleteTable();

document.getElementById('athleteForm').addEventListener('submit', function(event) {
  event.preventDefault();
  let name = document.getElementById('name').value.trim();
  let bibNo = document.getElementById('bibNoInput').value.trim();
  let batch = document.getElementById('batch').value.trim(); // Retrieve selected batch
  let gender = document.getElementById('gender').value;
  let ageCategory = document.getElementById('ageCategory').value;

  if (editIndex === -1) {
    // Check if the bib number already exists in the table
    if (isBibNumberExist(bibNo)) {
      alert("Bib number already exists in the athlete information.");
      return;
    }

    athletes.push({ name, bibNo, batch, gender, ageCategory }); // Include batch information
  } else {
    athletes[editIndex] = { name, bibNo, batch, gender, ageCategory };
    editIndex = -1;
  }

  renderAthleteTable();
  document.getElementById('name').value = ''; // Clear the athlete name field
  document.getElementById('bibNoInput').value = ''; // Clear the bib number field
});

    // Add this script to set the selected options based on the last selected values
    document.addEventListener('DOMContentLoaded', function() {
      let lastBatch = localStorage.getItem('lastBatch');
      let lastAgeCategory = localStorage.getItem('lastAgeCategory');
      if (lastBatch) {
        document.getElementById('batch').value = lastBatch;
      }
      if (lastAgeCategory) {
        document.getElementById('ageCategory').value = lastAgeCategory;
      }
    });

    function renderAthleteTable() {
      let tableBody = document.getElementById('athleteTableBody');
      tableBody.innerHTML = '';
      athletes.forEach(function(athlete, index) {
        let row = tableBody.insertRow();
        row.insertCell().innerText = athlete.name;
        row.insertCell().innerText = athlete.bibNo;
        row.insertCell().innerText = athlete.batch;
        row.insertCell().innerText = athlete.gender;
        row.insertCell().innerText = athlete.ageCategory;
        let editCell = row.insertCell();
        let editButton = document.createElement('button');
        editButton.innerText = 'Edit';
        editButton.onclick = function() { editAthlete(index); };
        editCell.appendChild(editButton);
      });
    }

    function editAthlete(index) {
      editIndex = index;
      let athlete = athletes[index];
      document.getElementById('name').value = athlete.name;
      document.getElementById('bibNoInput').value = athlete.bibNo;
      document.getElementById('batch').value = athlete.batch;
      document.getElementById('gender').value = athlete.gender;
      document.getElementById('ageCategory').value = athlete.ageCategory;
    }

function recordBibNumber() {
  let bibNumberInput = document.getElementById("bibNumber");
  let bibNumber = bibNumberInput.value.trim();
  if (bibNumber !== "") {
    let batch = findBatch(bibNumber);
    if (batch !== null) {
      logBatchTime(batch);
    } else {
      // Add the bib number to Batch A
      batch = 'A';
      logBatchTime(batch);
    }
    // Update dynamic lap headers after logging the batch time
    updateDynamicLapHeaders('A'); // Always update the lap headers for Batch A
    bibNumberInput.value = "";
    bibNumberInput.focus();
  } else {
    alert("Please enter a bib number.");
  }
}




  function isBibNumberExist(bibNumber) {
    // Check if the bib number already exists in the table
    return athletes.some(athlete => athlete.bibNo === bibNumber);
  }

  // Function to find the batch of the participant based on bib number
  function findBatch(bibNumber) {
    // Find the athlete with the provided bib number
    let athlete = athletes.find(athlete => athlete.bibNo === bibNumber);
    return athlete ? athlete.batch : null; // Return batch if athlete is found, otherwise return null
  }


  function calculateTotalTime(batch, bibNo) {
  let splitTimesArray = splitTimes[batch][bibNo];
  let totalTimeMillis = 0;
  
  // Sum up all the split times
  for (let i = 0; i < splitTimesArray.length; i++) {
    totalTimeMillis += timeStringToMillis(splitTimesArray[i]);
  }
  
  // Convert the total time back to the time string format
  return millisToTimeString(totalTimeMillis);
}




// Update the logBatchTime function to add data-time attribute to cells
function logBatchTime(batch) {
  // Get the current time from the corresponding stopwatch
  let currentTime = document.getElementById('timer' + batch).innerText;
  let bibNo = document.getElementById('bibNumber').value.trim(); // Get bib number
  let name = getNameFromBibNo(bibNo); // Get name from bib number

  // Initialize an array to store split times for the current batch
  if (!splitTimes[batch]) {
    splitTimes[batch] = {};
  }

  // Initialize an array to store split times for the current participant
  if (!splitTimes[batch][bibNo]) {
    splitTimes[batch][bibNo] = [];
  }

  // Add current split time to the array
  splitTimes[batch][bibNo].push(currentTime);

  // Find the table body for the corresponding batch
  let tbody = document.getElementById("tbody" + batch);

  // Find existing row for the bib number
  let existingRow = findRowByBibNo(tbody, bibNo);

  if (!existingRow) {
    // If the row doesn't exist, create a new row
    let newRow = tbody.insertRow();
    newRow.insertCell().innerText = bibNo;
    newRow.insertCell().innerText = name;
    newRow.insertCell(); // Add an empty cell for "Laps done"
  newRow.insertCell(); // Add an empty cell for "Total Time"
    existingRow = newRow;
  }

  // Add split time to the existing row
  let lapIndex = getLapIndex(existingRow);
  let splitTime = calculateSplitTime(batch, bibNo, lapIndex);
  let timeCell = existingRow.insertCell();
  timeCell.innerText = splitTime; // Add split time to the new cell


    let totalTimeCell = existingRow.cells[3];
    let totalTime = calculateTotalTime(batch, bibNo);
    totalTimeCell.innerText = totalTime;

  // Add data-time attribute to the cell
  timeCell.setAttribute('data-time', 'true');

  // Increment lap index for the next lap time
  lapIndex++;

  // Update the lap index attribute on the row for the next lap time
  existingRow.setAttribute('data-lap-index', lapIndex);

  // Update dynamic lap headers
  updateDynamicLapHeaders(batch, lapIndex);
  calculateLapTimes(batch, bibNo);
}

    

function updateDynamicLapHeaders(batch) {
  // Find the table header row for lap headers in the corresponding batch
  let headerRow = document.querySelector("#table" + batch + " thead tr");

  // Clear existing lap headers
  headerRow.innerHTML = "";

  // Add static headers (Bib No, Name, Total Time)
  let bibNoHeader = document.createElement("th");
  bibNoHeader.textContent = "Bib No";
  headerRow.appendChild(bibNoHeader);

  let nameHeader = document.createElement("th");
  nameHeader.textContent = "Name";
  headerRow.appendChild(nameHeader);
  
      let totalLapsHeader = document.createElement("th"); // Create new header element
  totalLapsHeader.textContent = "Total Laps"; // Set its text content
  headerRow.appendChild(totalLapsHeader); // Append it to the header row
  
   let totalTimeHeader = document.createElement("th");
    totalTimeHeader.textContent = "Total Time";
    headerRow.appendChild(totalTimeHeader);
  
  


  // Initialize an array to store the number of laps for each participant
  let maxLapsArray = [];

  // Iterate over all split times for the current batch
  if (splitTimes[batch]) {
    Object.values(splitTimes[batch]).forEach(splitTimeArray => {
      maxLapsArray.push(splitTimeArray.length);
    });
  }

  // Find the maximum number of laps across all participants
  let maxLaps = Math.max(...maxLapsArray);

  // Add dynamic lap headers based on maxLaps
  for (let i = 1; i <= maxLaps; i++) {
    let lapHeader = document.createElement("th");
    lapHeader.textContent = "Lap " + i;
    headerRow.appendChild(lapHeader);
  }
}


function calculateLapTimes(batch, bibNo) {
    const lapStartTime = document.querySelector(`#tbody${batch} tr[data-bib="${bibNo}"] td:nth-child(4)`);
    const lapEndTime = document.querySelector(`#tbody${batch} tr[data-bib="${bibNo}"] td:nth-child(5)`);

    if (lapStartTime && lapEndTime) {
        const startTime = lapStartTime.innerText.trim();
        const endTime = lapEndTime.innerText.trim();
        
        // Your calculation logic here
    } else {
        console.error("Unable to find lap start or end time for batch " + batch + ", bibNo " + bibNo);
    }
}


    
function calculateSplitTime(batch, bibNo, lapIndex) {
  let previousSplitTime = '00:00:00.00';
  
  // Retrieve the previous split time from the array
  if (splitTimes[batch] && splitTimes[batch][bibNo] && lapIndex > 0) {
    previousSplitTime = splitTimes[batch][bibNo][lapIndex - 1];
  }
  
  let currentTime = document.getElementById('timer' + batch).innerText;
  return calculateTimeDifference(previousSplitTime, currentTime);
}



function calculateTimeDifference(previousTime, currentTime) {
  // Convert time strings to milliseconds and calculate the difference
  let previousMillis = timeStringToMillis(previousTime);
  let currentMillis = timeStringToMillis(currentTime);
  console.log("Previous Milliseconds:", previousMillis); // Log previous milliseconds
  console.log("Current Milliseconds:", currentMillis); // Log current milliseconds
  let difference = currentMillis - previousMillis;
  console.log("Difference (Milliseconds):", difference); // Log difference in milliseconds

  // Convert difference back to time string format
  let splitTime = millisToTimeString(difference);
  console.log("Split Time:", splitTime); // Log split time
  return splitTime;
}

    function timeStringToMillis(timeString) {
      let parts = timeString.split(':');
      let hours = parseInt(parts[0]) * 3600000;
      let minutes = parseInt(parts[1]) * 60000;
      let seconds = parseFloat(parts[2]) * 1000;
      return hours + minutes + seconds;
    }

    function millisToTimeString(milliseconds) {
      let hours = Math.floor(milliseconds / 3600000);
      milliseconds %= 3600000;
      let minutes = Math.floor(milliseconds / 60000);
      milliseconds %= 60000;
      let seconds = Math.floor(milliseconds / 1000);
      milliseconds %= 1000;
      return pad(hours) + ':' + pad(minutes) + ':' + pad(seconds) + '.' + pad(Math.floor(milliseconds / 10));
    }
    
    
  function findRowByBibNo(tbody, bibNo) {
    // Find existing row for the bib number
    for (let i = 0; i < tbody.rows.length; i++) {
      let row = tbody.rows[i];
      if (row.cells[0].innerText === bibNo) {
        return row;
      }
    }
    return null; // Return null if row is not found
  }

  function getLapIndex(row) {
    // Get the lap index from the data attribute or default to 0
    let lapIndex = parseInt(row.getAttribute('data-lap-index')) || 0;
    return lapIndex;
  }

  function getNameFromBibNo(bibNo) {
    // Find the athlete with the provided bib number
    let athlete = athletes.find(athlete => athlete.bibNo === bibNo);
    return athlete ? athlete.name : ''; // Return name if athlete is found, otherwise return empty string
  }

  let timers = {};
  let isRunning = {};
let splitTimes = {};

  function startTimer(batch) {
    if (!isRunning[batch]) {
      let startTime = Date.now();
      timers[batch] = setInterval(function() {
        let elapsedTime = Date.now() - startTime;
        updateTimer(batch, elapsedTime);
      }, 10);
      isRunning[batch] = true;
    }
  }

  function stopTimer(batch) {
    clearInterval(timers[batch]);
    isRunning[batch] = false;
  }

  function resetTimer(batch) {
    stopTimer(batch);
    document.getElementById('timer' + batch).innerText = '00:00:00.00';
  }

  function updateTimer(batch, elapsedTime) {
    let milliseconds = Math.floor((elapsedTime % 1000) / 10);
    let seconds = Math.floor((elapsedTime / 1000) % 60);
    let minutes = Math.floor((elapsedTime / (1000 * 60)) % 60);
    let hours = Math.floor((elapsedTime / (1000 * 60 * 60)) % 24);

    document.getElementById('timer' + batch).innerText = 
      pad(hours) + ':' + pad(minutes) + ':' + pad(seconds) + '.' + pad(milliseconds);
  }

  function pad(number) {
    if (number < 10) {
      return '0' + number;
    }
    return number;
  }

  let nextBatchIndex = 1;


function handleKeyPress(event) {
  if (event.key === "Enter") {
    recordBibNumber();
  }
}

    function clearTable() {
      // Prompt the user for confirmation
      let confirmation = confirm("Are you sure you want to clear the table?");
      
      // If the user confirms, clear the table
      if (confirmation) {
        let tableBodies = document.querySelectorAll("tbody");
        tableBodies.forEach(tableBody => {
          tableBody.innerHTML = ""; // Clear the table body content
        });
      }
    }






 document.addEventListener('DOMContentLoaded', function() {
    // Get all tables within the stopwatches section
    let tables = document.querySelectorAll('#stopwatches table');

    // Iterate over each table
    tables.forEach(function(table) {
      // Get the table body of the current table
      let tbody = table.querySelector('tbody');

      // Add event listener to the table body
      tbody.addEventListener('dblclick', function(event) {
        // Check if the double-clicked element is a cell
        if (event.target.tagName === 'TD') {
          // Make the cell editable
          let cell = event.target;
          cell.contentEditable = true;
          // Add a border to indicate editing
          cell.style.border = '1px solid #ccc';
          // Focus on the cell to allow immediate editing
          cell.focus();
        }
      });

      // Add touch event listeners to enable long press for mobile devices
      tbody.addEventListener('touchstart', function(event) {
        // Set a timeout to detect long press (500 milliseconds)
        touchTimer = setTimeout(function() {
          // Check if the touched element is a cell
          if (event.target.tagName === 'TD') {
            // Make the cell editable
            let cell = event.target;
            cell.contentEditable = true;
            // Add a border to indicate editing
            cell.style.border = '1px solid #ccc';
            // Focus on the cell to allow immediate editing
            cell.focus();
          }
        }, 500); // Adjust the timeout duration as needed
      });

      // Clear the touch timer if the touch ends before the timeout
      tbody.addEventListener('touchend', function() {
        clearTimeout(touchTimer);
      });

      // Add event listener to save edited content when blurring from cell
      tbody.addEventListener('blur', function(event) {
        // Check if the blurred element is a cell
        if (event.target.tagName === 'TD') {
          // Save the edited content
          saveEditedContent(event.target);
        }
      });

      // Add event listener to save edited content when pressing Enter
      tbody.addEventListener('keypress', function(event) {
        if (event.key === 'Enter') {
          // Check if the focused element is a cell
          if (document.activeElement.tagName === 'TD') {
            // Prevent the default behavior of pressing Enter in a contenteditable element
            event.preventDefault();
            // Save the edited content
            saveEditedContent(document.activeElement);
          }
        }
      });
    });
  });

  function saveEditedContent(cell) {
    // Remove the border to indicate the end of editing
    cell.style.border = 'none';
    // Make the cell non-editable
    cell.contentEditable = false;
  }



  function startNewBatch() {
    let batchName = String.fromCharCode(65 + nextBatchIndex); // Convert index to character code (A=65, B=66, C=67, ...)
    createNewBatch(batchName);
    nextBatchIndex++;
  }

function createNewBatch(batch) {
  let newDiv = document.createElement('div');
  newDiv.classList.add('stopwatch');
  newDiv.id = 'batch' + batch;
  newDiv.innerHTML = `
    <h2>Batch ${batch} Stopwatch</h2>
    <div id="timerContainer">
      <p class="solid" id="timer${batch}">00:00:00.00</p>
    </div><br>
    <button class="green-button" onclick="startTimer('${batch}')">Start</button>
    <button class="green-button" onclick="stopTimer('${batch}')">Stop</button>
    <button class="green-button" onclick="resetTimer('${batch}')">Reset</button>
    <table id="table${batch}">
      <thead>
        <tr>
             <th>Bib No</th>
            <th>Name</th>
           <th>Total Laps</th>
           <th>Total Time</th>      
          <!-- Dynamic lap headers -->
          <th id="lapHeaders${batch}"></th>
        </tr>
      </thead>
      <tbody id="tbody${batch}"></tbody>
    </table>
  `;
  document.getElementById('stopwatches').appendChild(newDiv);

  // Add event listeners to the newly created table
  let newTable = newDiv.querySelector('table');
  let newTbody = newTable.querySelector('tbody');

  // Add event listener to the table body
  newTbody.addEventListener('dblclick', function(event) {
    // Check if the double-clicked element is a cell
    if (event.target.tagName === 'TD') {
      // Make the cell editable
      let cell = event.target;
      cell.contentEditable = true;
      // Add a border to indicate editing
      cell.style.border = '1px solid #ccc';
      // Focus on the cell to allow immediate editing
      cell.focus();
    }
  });

  // Add event listener to save edited content when blurring from cell
  newTbody.addEventListener('blur', function(event) {
    // Check if the blurred element is a cell
    if (event.target.tagName === 'TD') {
      // Save the edited content
      saveEditedContent(event.target);
    }
  });

  // Add event listener to save edited content when pressing Enter
  newTbody.addEventListener('keypress', function(event) {
    if (event.key === 'Enter') {
      // Check if the focused element is a cell
      if (document.activeElement.tagName === 'TD') {
        // Prevent the default behavior of pressing Enter in a contenteditable element
        event.preventDefault();
        // Save the edited content
        saveEditedContent(document.activeElement);
      }
    }
  });
}

  
</script>

</body>
</html>

I am not an expert, and doing this with the help of ChatGPT. But it have its limitations. Cannot get the sum of each batch to calculate correctly.