Sunday, 1 November 2026

AI Doomsday Scenario: Truth or Hogwash? (Part 1/2)

The month of September in 2026 was almost completely dominated by one headline, in the world of tech, that is. Drumroll...

The Artificial Intelligence Doomsday Scenario.

Sam Altman, Dario Amodei and Elon Musk - all CEOs of AI tech companies, all came out to warn us that AI tech is dangerous and could result in wiping out all of humanity, and called for regulatory measures. And I find all this less than convincing. 

It's not that I disagree that regulation is needed. It's overdue, even. But the timing is... interesting. It just seems awfully convenient that this comes at a time where OpenAI and Anthropic are trying to IPO (OpenAI's plans seem to have been curtailed at this time of writing), and China is producing cheaper comparable models and giving away the services for free. These three wouldn't be trying to cripple competition via regulation, would they?

Here's the problem, tech CEOs. People with an IQ higher than the average box of rocks, don't just want to listen to your dramatic fantasies. We want to be given a plausible framework for how this could happen. Who do you think you're talking to, superstitious villagers doing a rain dance?

We're too old for bedtime stories. Show us the math. You said AI will being about the end of the world. That's a big statement. Go on, walk us through it.

Dangerous things.

Any idiot can tell me that AI is potentially dangerous. I could be a terrible driver, and even worse marksman, and still be able to tell you that cars and guns can be lethal. "AI has the capability to do great harm" isn't exactly an Earth-shattering revelation. You're major decision-makers in famous tech companies. I expect more.

Only Amodei has even made the attempt. It still sounds like science-fiction, but to be fair, it's more than either Altman or Musk have given us. Though, come to think of it, wasn't Musk the same guy who said AI should compile binaries instead of generating code? You know what, I think we'll do just fine without his input.

Amodei also called for curbs on selling chips to China.

The China issue

I would caution against putting China in this position. Remember Huawei back in 2018? USA put them in a situation where they'd have to create their own OS instead of leveraging off Android. How'd that turn out? I'll tell you how it turned out - Huawei came up with HarmonyOS and have now conquered the China market.

China has the raw materials and the energy resources. They're also known as the world's fastest builders, a reputation cemented during the COVID crisis. And also the world's foremost manufacturers, a reputation cemented long before the COVID crisis. If, on top of their capabilities and resources, they're given the motivation, do these US companies think things will go well for them? Take note of the words "capability" and "motivation" - they'll be relevant again very soon.

Building fast!

Also, speaking of Huawei, they're actually working really hard to become an alternative to Nvidia, with their work on the Ascend series chip. So if anyone thought the HarmonyOS analogy was weak, here's a fact for you: Huawei has already started building chips, not just mobile OS. And they appear to have accelerated their plans. 

Jensen Huang takes the opposite stance

Jensen Huang, CEO of Nvidia, the firm selling GPUs to the various players, has come out to say that "there's a 0% chance of that happening by 2030", "scaring people is unnecessary, it is irresponsible" and "not grounded in science". 


That elicited a chuckle. I'm a great admirer of Jensen Huang with his distinguished grey hair, suave leather jacket and impressive ability to ooze sincerity while spewing complete horseshit. Guy basically just wants to sell more chips. It's not wrong. It's literally his job. I'm just laying out the obvious incentive.

All of these people are saying things I agree with - more regulation, apocalypse unlikely - albeit for eyebrow-raising (and very self-serving) premises. I'm not saying that they're lying to our faces, exactly... but when the timing is this convenient and the incentives this obvious, we're entitled to examine the claims. Scratch that; we're obligated to.

It strikes me that both those claiming a doomsday scenario (Altman, Amodei, Musk) and those claiming otherwise (Huang) haven't really supplied any kind of reasoning chain as to why it will or won't happen. It's cute to watch them both play this game.

What do I mean by "reasoning chain"?

Here's an example. Amodei has cited the recursive self-improvement of AI and the recent cybersecurity breaches known as OAI-HF as scaled-down examples of what could happen. He's supplied the capability.

However, he's missing the propensity - what are the tendencies of AI, and what would motivate it to use those capabilities against us? Does AI eventually discover that human farts are a great source of fuel and decide to farm us? There you go - capability and motivation. These are important.

After capability and propensity, we then look at opportunity. What are the mistakes we would have to make, in order for a highly-capable, properly-motivated AI to get its opening?

No chain.

Without these three parts, there is no chain. Sorry, Amodei. Maybe you should have gotten Claude to help you with that.

One last obvious thing I want to point out. Altman says we should slow down AI development. OK. What's he waiting for, permission? Does he need a signature somewhere for him to proceed? He's literally the CEO of OpenAI.

Is he waiting for everyone else to slow down at the same time? China, too? Well, I guess we're all screwed, then.

Next

Jacob Coxon and the ten-year warning.

Thursday, 8 October 2026

TeochewThunder: Year Twelve (Part 2/2)

In terms of views, I wouldn't say 2026 was a worse year for this blog. If anything, it performed better than 2025 and on par with 2024, so far anyway.

And as I noted earlier, there were quite a few posts from His Teochewness that showed open contempt for... well, shit, whoever happened to be pissing me off at the time. And because some of these were from way back years ago and I'd had time to properly ruminate over why I got pissed off, the language is arguably a lot less intense but heaps more damaging than if I'd gone in red hot. Because, then, I wasn't wildly throwing haymakers; I was landing kidney shots and throat jabs.

High Performers

These were the ones that got the most attention. And honestly, I'm heartened that they weren't even the most controversial ones. They mostly centered around my home exploration of Singapore. Of course, my Black Mirror Film Reviews have always been disproportionately popular.

Lots of attention.

- A Software Developer's Vacation in... Singapore
- Film Review: Black Mirror Series Seven
- Film Review: Black Mirror Series Seven, Redux
- A Software Developer's Vacation In Singapore: Coast-to-Coast Trail Edition

The one sole standout was Finding Your Purpose Professionally, which involved me dunking on both older and younger workers alike, because they annoy me in equal measure, for different reasons.

Surprise standouts

These are next in line to be mentioned because they got a pretty big audience as well. Some of the attention was expected because I courted it with every F-bomb and verbal strike. (Making the contact tracing effort mandatory, redux and Hiring remote foreigners? Slow your roll, hotshot and The Case against Hunger, Enthusiasm and Other Useless Buzzwords).

This one, Ten Reasons To Keep Treating Job Interviews As Target Practice, in particular, was inflammatory, but I also like to think it was helpful.

The same can't be said of Musk's Macrohard is as silly as it sounds. I didn't intend for it to be helpful. Elon Musk said something stupid (something staggeringly foolish that offended me as a programmer) and I called him out on it. 

Then there were technical pieces like Five Image File Formats For The Web and More ways to declare colors in CSS! and I really couldn't tell you why they turned out as well as the foul-mouthed ones. And then there's Named Versus Positional Notation in Functions and Methods. One of the popular ones. I mean, it's such riveting stuff, right?

Good amounts of attention.

Working has different goals from working out and JavaScript's non-existent relationship to Java and Bolt CEO's dangerous logic for axing his entire HR Team were more philosophical than rude. And yet... boom!

The Case of the Awkward Tech Interviewee was another war story which I didn't think was all that compelling, but I guess reality proved me wrong, there.

Duds

Obviously there were times I was disappointed with the turnout because I really felt my effort deserved more. This could have been down to any number of reasons, chief of which is that my writing just isn't as compelling as I gave it credit for.

For example, Seriously, why try to cook Sam Altman? is a post I thought was great. It had one of my favorite targets in my crosshairs and in it I savaged both Altman and his would-be assassin. No bites. Or just fewer than I imagined I'd get.

Very little attention.

Artificial Intelligence and Its Effect on Entry-level Hiring was another one I put a fair bit of heart into. Perhaps it needs more time to gestate, having just been released last month.

Five Funny Tech CEO Nicknames was another one I thought would be a sure hit. No such luck. It seems a guy can't even rely on making fun of CEOs any more, to get eyeballs.

Onward to Year 13!

This really wasn't a bad blogging year at all. If next year is anywhere as good, I'd consider myself blessed.

I make all these critiques of my blogposts while understanding that I'm really nobody in the big picture and attention isn't something I'm entitled to, even if by some miracle I could handle it.

XII-ding expectations,
T___T

Monday, 5 October 2026

TeochewThunder: Year Twelve (Part 1/2)

It's time for my annual blogging break! Let's look at what 2026 brought us - lots of tech news (mostly about Artificial Intelligence), quite a bit of jQuery and even a bit of NodeJS.

Happy 12th blogiversary!

Twelve years to the day I got off my ass and started this blog. Seems unbelievable. In that time, I've bought my own place, gotten married, quit smoking and learned maybe five different languages - not all of them programming ones.

Those familiar with the TeochewThunder tradition will know that October is the month I take a break and gear up for the end of the year.

Tone

I'll begin the year-end retrospective by conceding that I've been making rather inflammatory posts recently. Specifically towards small biz owners.

The small business owner and I have a complicated relationship. On one hand, I realize that it isn't easy to run a business, and it's far more pleasurable to be working for a small outfit than a huge one with tons of stifling rules. On the other hand, I hate it when small potatoes give themselves airs... and some of these are tiny spuds acting like they're the shit. When in all likelihood, they're just shit.

Tiny spuds.

Suffice to say, if even one of these fellas reads my stuff and ends up going "fuck this guy", I'd pat myself on the back. Really, what are they gonna do? I'm practically retired.

That said, I've managed to keep the profanity somewhat minimal. Not as minimal as last year, obviously, but I'm proud of myself for the effort. That's not to say I can't be provocative and offensive if I don't swear, because I absolutely fucking can. I just don't want to end up pigeonholing myself.

Speaking of Artificial Intelligence...

Not that anyone asked, but all this content has continued to be written by me. None of this was written by AI. That distinction is important. On Social Media, I've noticed a whole slew of long-winded posts that scream "LLM Vomit".

Imperfect as my writing is, it's not that. Sure, I do consult an LLM on content. I run my writing through Claude and ChatGPT and even Gemini. AI checks for typos and unverifiable facts, and cautions me on possible inflammatory speech, and tells me that some people may get their precious feelings hurt. In the final case, I may even smile, and ramp up the offensiveness.

You're offended? I'll
show you offended.

All of that is a far cry from letting an LLM generate text for me while I tweak the wording. You may get a lot of crap from me, but you'll never get that kind of crap.

I don't want to give you that stuff, but the fact of the matter is, I couldn't even if I wanted to. Even after feeding the LLMs my writing and asking them to reproduce it, I've yet to find one that even comes close. That's largely because LLMs try to be as correct and inoffensive as possible. And as longtime readers know, these aren't always top on my list of priorities.

Morever, using an LLM for output generation is just superfluous. I'm not in a position where I need those productivity gains. This blog is not for profit; if I can't even be authentic in this situation, that's just sad.

I don't feel quite the same way about AI image generation, though. Because none of you have ever been under any kind of impression that I took those stock photos myself, and generating images gives me what I want without having to hunt endlessly for it. The only difference is that some photographers won't be receiving my paltry monetary thanks, but it isn't as if they would starve otherwise.

Web Tutorials

As mentioned earlier, there was quite a bit of jQuery this year. That's for good reason.

There's increasingly little value in using vanilla JavaScript to demonstrate stuff. I have to jump through too many hoops and explain too much. And in this day and age, using getElementById() feels positively primitive. Shit, in this day and age, jQuery isn't particularly cutting edge either.

Getting primitive.

jQuery gets me there faster with less fuss, and sometimes that matters more. After years of using vanilla JavaScript, it's what I cut my teeth on and I'll always have a soft spot for it, but at some point pragmatism needs to win out.

There's also some NodeJS and a bit of VueJS. I'm not an ideologue where code is concerned; whatever works best in the given context gets the nod. But above all, I don't want to be a one-trick pony. That's almost tantamount to career suicide in my industry. Not that I have a long fruitful career to look forward to at the age of 49, but it's the principle of the thing.

Other content

The dry technical content (outside of Web Tutorials) is still present in the blog, but it's been drying up a bit. Not to say I don't still enjoy writing those even though nobody seems to enjoy reading them, if the views are any indication, but I realize that AI could do a far better job than me of explaining that stuff.

Still, features like Spot The Bug are still going fairly consistently even though I seem to be doing JavaScript almost exclusively (yes, different frameworks and libraries, but still JavaScript).

One thing I've been doing more of, is lifestyle stuff. Things I've been doing outside of work. It used to be I'd do this a couple times a year by vacationing out of the country, but lately I've found that Singapore can be just as interesting without having to break out my trusty old passport.

Wheels, baby, wheels!

And part of it is just me discovering, after having quit smoking, that I like being allowed in non-smoking areas like parks and nature reserves. Huh. Go figure. Funny the directions life takes us. As it is, I'm discovering a whole new side of Singapore on bicycle. And somehow, part of me just can't stop comparing all this shit to software architecture. It could be that being a software dev has allowed me to appreciate the art of building all this, within the constraints of a tiny island.

Back to Artificial Intelligence

The reason why I've been writing so much about AI is because, love it or hate it, it's hard to simply dismiss it. It's permeating code, Social Media, employment and technology - four subjects this blog tackles regularly.

I can't seem to get by a day on YouTube without listening to some take on AI. How LLMs are good, how they're bad, how the AI Bubble is about to burst...

Code bubble.

Also, I have opinions. Shocking, right?

As a software developer, I also have to deal with colleagues and bosses who range from wildly enthusiastic, to cautiously optimistic, to staunchly negative, and none of these positions is inherently wrong, which makes things a lot more interesting than your typical flat earth debate. The situation changes by the day, and that's genuinely intriguing.

For example, right now there's an ongoing Doomsday discussion as to whether AI will bring about the end of humankind by the end of the decade. By the time I publish my two cents on this, it will be November and my perspective will be stale. But whatevs, it's not like the Earth has stopped spinning on its axis waiting for me to release my opinion to the world, eh?

Next

Hits and misses of Year 12.

Tuesday, 29 September 2026

Web Tutorial: The List View (Part 4/4)

So far we have been working solely with March 2025 data. It's a limited subset of the entire dataset, which is why we could perform paging, sorting and all that, in the front-end. It was a deliberate design decision. This pattern will not scale if you have an unlimited dataset.

With that in mind, we will provide an interface for the user to get the rest of the data. In the topmost div, listFilters, add this HTML. It's a drop-down list.
<div id="listFilters">
  <label>
    Period:
    <select id="ddlPeriod">
      <option value="3">March 2025</option>
      <option value="4">April 2025</option>
    </select>          
  </label>


  <hr />
</div>  


So far, we only have March and April 2025 records, so this is easy - just have the drop-down list run getRecords() with 2025 as the argument for year, and the selected value as the argument for month.
<div id="listFilters">
  <label>
    Period:
    <select id="ddlPeriod" onchange="lv.getRecords(2025, this.value);">
      <option value="3">March 2025</option>
      <option value="4">April 2025</option>
    </select>          
  </label>

  <hr />
</div>  


The CSS for labels is just to bold the text.
  .asc:after
  {
    content: "\25B2";
  }

  .desc:after
  {
    content: "\25BC";
  }

  label
  {
    font-weight: bold;
  }

</style>


Don't need to do much else.


There, these are April borrowing records!


Now for the Search function

Add a space and another label tag, and insert the txtSearch textbox.
<div id="listFilters">
  <label>
    Period:
    <select id="ddlPeriod" onchange="lv.getRecords(2025, this.value);">
      <option value="3">March 2025</option>
      <option value="4">April 2025</option>
    </select>          
  </label>

   &nbsp;

  <label>
    Search:
    <input id="txtSearch" placeholder="Search term, e.g, orange" />
  </label>


  <hr />
</div>  


Have it run setSearch() with its current value, whenever someone types stuff in the box.
<div id="listFilters">
  <label>
    Period:
    <select id="ddlPeriod" onchange="lv.getRecords(2025, this.value);">
      <option value="3">March 2025</option>
      <option value="4">April 2025</option>
    </select>          
  </label>

   

  <label>
    Search:
    <input id="txtSearch" placeholder="Search term, e.g, orange" value="" oninput="lv.setSearch(this.value);" />
  </label>

  <hr />
</div>  


This is the setSearch() method. It has a parameter, searchTerm. We start with a return statement.
setPageSize: function(pageSize)
{
  this.pageSize = pageSize;
  this.pageNo = 1;
  this.filterRecords();
  this.renderPageInfo();

  return;
},
setSearch: function(searchTerm)
{
  return;
},

toggleSort: function(col, id)
{


Now we set the search property. If searchTerm is longer than 2 characters, we convert to lowercase and set it. If not, we set search to an empty string.
setSearch: function(searchTerm)
{
  this.search = (searchTerm.length > 2 ? searchTerm.toLowerCase() : "");

  return;
},


Then we run filterRecords() and renderPageInfo().
setSearch: function(searchTerm)
{
  this.search = (searchTerm.length > 2 ? searchTerm.toLowerCase() : "");
  this.filterRecords();
  this.renderPageInfo();


  return;
},


In filterRecords(), before sorting, we filter according to the search term. We first check if action needs to be taken. If search is less than 2 characters, we won't bother.
filterRecords: function()
{
  $("#listRows").html("");

  var filtered = structuredClone(this.records);

  if (this.search.length > 2)
  {

  }


  if (this.sort.length > 0)
  {
    this.sort.forEach(


We then filter the filtered array using the filter() method.
if (this.search.length > 2)
{
  filtered = filtered.filter
  (
    (x) =>
    {

    }
  );

}


We check against the title, category, author and name properties, all converted to lowercase, against the search property, and return true if there is any match.
if (this.search.length > 2)
{
  filtered = filtered.filter
  (
    (x) =>
    {
      return (
        x.title.toLowerCase().indexOf(this.search) > -1 ||
        x.category.toLowerCase().indexOf(this.search) > -1 ||
        x.author.toLowerCase().indexOf(this.search) > -1 ||
        x.name.toLowerCase().indexOf(this.search) > -1

      );
    }
  );
}


Let's also remove the red outlines since we hve no more layout changes to make.
div {outline: 0px solid red;}


Let's try typing in "orange" like the nice placeholder text recommends. Start with "or". See how nothing happens.


Now add "a" for "ora". Now see the results change dramatically. They display because "ora" was in the title, or because the author was "Dora" "Deborah", "Flora" or "Coral".

Try the full term "orange". Now there's only one result!


Now switch the records to April 2025. Can you see the logic behind this?

We're done here! Thanks for participating!

Play around with your list view. Try out the features we made.

Orange you glad we did this?!
T___T

Sunday, 27 September 2026

Web Tutorial: The List View (Part 3/4)

Welcome back. Let's deal with sorting now, by adding to the sort array.

The default sorting for the list view will be first alphabetically by title, then by most recently borrowed. Each sorting detail is an object of its own. The sequence matters.
const lv =
{
  pageSize: 50,
  pageNo: 1,
  sort: [ {"col": "title", "dir": "asc"}, {"col": "borrowdate", "dir": "desc"} ],
  search: "",
  records: [],


Now, in filterRecords(), we want action only if there's stuff in sort. Therefore, we insert a conditional block here. Then we iterate through sort using a forEach() loop.
filterRecords: function()
{
  $("#listRows").html("");

  var filtered = structuredClone(this.records);

  if (this.sort.length > 0)
  {
    this.sort.forEach(
      (x) =>
      {

      }
    );
  }


  var startIndex = ((this.pageNo - 1) * this.pageSize);


This next If block tests if "title", "author", "name" or "category" are the columns being sorted here - they're text data, so the comparison will be string-based.
filterRecords: function()
{
  $("#listRows").html("");

  var filtered = structuredClone(this.records);

  if (this.sort.length > 0)
  {
    this.sort.forEach(
      (x) =>
      {
        if (["title", "author", "name", "category"].indexOf(x.col) > -1)
        {
                
        }

      }
    );
  }

  var startIndex = ((this.pageNo - 1) * this.pageSize);


We check for dir. If the value is "asc" or "desc", we compare accordingly using the localeCompare() method.
filterRecords: function()
{
  $("#listRows").html("");

  var filtered = structuredClone(this.records);

  if (this.sort.length > 0)
  {
    this.sort.forEach(
      (x) =>
      {
        if (["title", "author", "name", "category"].indexOf(x.col) > -1)
        {
          if (x.dir == "asc") filtered.sort((a, b) => a[x.col].localeCompare(b[x.col]));
          if (x.dir == "desc") filtered.sort((a, b) => b[x.col].localeCompare(a[x.col]));    
              
        }
      }
    );
  }

  var startIndex = ((this.pageNo - 1) * this.pageSize);


For "borrowdate", "returndate" or "returnby", these are date comparisons. We convert the strings to Date objects and compare.
filterRecords: function()
{
  $("#listRows").html("");

  var filtered = structuredClone(this.records);

  if (this.sort.length > 0)
  {
    this.sort.forEach(
      (x) =>
      {
        if (["title", "author", "name", "category"].indexOf(x.col) > -1)
        {
          if (x.dir == "asc") filtered.sort((a, b) => a[x.col].localeCompare(b[x.col]));
          if (x.dir == "desc") filtered.sort((a, b) => b[x.col].localeCompare(a[x.col]));                  
        }

        if (["borrowdate", "returndate", "returnby"].indexOf(x.col) > -1)
        {
          if (x.dir == "asc") filtered.sort((a, b) => new Date(a[x.col]) - new Date(b[x.col]));
          if (x.dir == "desc") filtered.sort((a, b) => new Date(b[x.col]) - new Date(a[x.col]));        
        }

      }
    );
  }

  var startIndex = ((this.pageNo - 1) * this.pageSize);


"fine" is currently the only numerical column sortable, and that one's the most straightforward.
filterRecords: function()
{
  $("#listRows").html("");

  var filtered = structuredClone(this.records);

  if (this.sort.length > 0)
  {
    this.sort.forEach(
      (x) =>
      {
        if (["title", "author", "name", "category"].indexOf(x.col) > -1)
        {
          if (x.dir == "asc") filtered.sort((a, b) => a[x.col].localeCompare(b[x.col]));
          if (x.dir == "desc") filtered.sort((a, b) => b[x.col].localeCompare(a[x.col]));                  
        }

        if (["borrowdate", "returndate", "returnby"].indexOf(x.col) > -1)
        {
          if (x.dir == "asc") filtered.sort((a, b) => new Date(a[x.col]) - new Date(b[x.col]));
          if (x.dir == "desc") filtered.sort((a, b) => new Date(b[x.col]) - new Date(a[x.col]));        
        }

        if (["fine"].indexOf(x.col) > -1)
        {
          if (x.dir == "asc") filtered.sort((a, b) => a[x.col] - b[x.col]);
          if (x.dir == "desc") filtered.sort((a, b) => b[x.col] - a[x.col]);        
        }

      }
    );
  }

  var startIndex = ((this.pageNo - 1) * this.pageSize);


Now take a close look - the records are ordered by the column "Borrowed", most recent dates first. Then take a look at those records borrowed on "2025-03-23". Note that the titles are sorted alphabetically in that group!


Cool! Next, you'll want to properly display what's being sorted, and have a mechanism to change the sorting. In the CSS, allow sortButton to turn red if hovered over as well.
  .sortButton, .noButton
  {
    width: auto;
    padding: 0px;
    height: 1.5em;
    font-size: 1em;
    background: transparent;
    border: none;
    color: rgb(0, 0, 0);
  }

  .pageButton:hover, .sortButton:hover
  {
    color: rgb(255, 0, 0);
  }
</style>


Then we use the after pseudoselector on sortButton. We want it to be a tiny square that has some spacing from the end of the text. By default, there is no text content.
  .pageButton:hover, .sortButton:hover
  {
    color: rgb(255, 0, 0);
  }

  .sortButton:after
  {
    width: 10px;
    height: 10px;
    margin-left: 5px;
    display: inline-block;
    content: "";
  }

</style>


But then we declare asc and desc as new CSS classes with pseudoselectors. In these cases, asc uses a upwards pointing triangle as content and desc uses a triangle pointing down.
  .pageButton:hover, .sortButton:hover
  {
    color: rgb(255, 0, 0);
  }

  .sortButton:after
  {
    width: 10px;
    height: 10px;
    margin-left: 5px;
    display: inline-block;
    content: "";
  }

  .asc:after
  {
    content: "\25B2";
  }

  .desc:after
  {
    content: "\25BC";
  }

</style>


Add asc to btnSortTitle and desc to btnSortBorrowdate.
<div id="listHeader" class="listViewColumns">
  <div><button class="noButton">No</button></div>
  <div><button id="btnSortTitle" class="sortButton asc">Title</button></div>
  <div><button id="btnSortAuthor" class="sortButton">Author</button></div>
  <div><button id="btnSortCategory" class="sortButton">Category</button></div>
  <div><button id="btnSortName" class="sortButton">Name</button></div>
  <div><button id="btnSortBorrowdate" class="sortButton desc">Borrowed</button></div>
  <div><button id="btnSortReturnBy" class="sortButton">Return By</button></div>
  <div><button id="btnSortReturndate" class="sortButton">Returned</button></div>
  <div><button id="btnSortFine" class="sortButton">Fine</button></div>
</div>


Look at the headings. Here's your visual indicator!


Make sure each heading (other than the "No" column) runs the toggleSort() method when clicked, with the actual column name and button id passed in as arguments.
<div id="listHeader" class="listViewColumns">
  <div><button class="noButton">No</button></div>
  <div><button id="btnSortTitle" class="sortButton asc" onclick="lv.toggleSort('title', this.id)">Title</button></div>
  <div><button id="btnSortAuthor" class="sortButton" onclick="lv.toggleSort('author', this.id)">Author</button></div>
  <div><button id="btnSortCategory" class="sortButton" onclick="lv.toggleSort('category', this.id)">Category</button></div>
  <div><button id="btnSortName" class="sortButton" onclick="lv.toggleSort('name', this.id)">Name</button></div>
  <div><button id="btnSortBorrowdate" class="sortButton desc" onclick="lv.toggleSort('borrowdate', this.id)">Borrowed</button></div>
  <div><button id="btnSortReturnBy" class="sortButton" onclick="lv.toggleSort('returnby', this.id)">Return By</button></div>
  <div><button id="btnSortReturndate" class="sortButton" onclick="lv.toggleSort('returndate', this.id)">Returned</button></div>
  <div><button id="btnSortFine" class="sortButton" onclick="lv.toggleSort('fine', this.id)">Fine</button></div>
</div>


Create toggleSort() with a return statement.
setPageSize: function(pageSize)
{
  this.pageSize = pageSize;
  this.pageNo = 1;
  this.filterRecords();
  this.renderPageInfo();

  return;
},
toggleSort: function(col, id)
{
  return;
},      
  
filterRecords: function()
{


We declare currentIndex and set it to -1. Then we clear the column's button of both asc and desc CSS classes, returning it to a visually neutral state.
toggleSort: function(col, id)
{
  var currentIndex = -1;
  $("#" + id)
  .removeClass("asc")
  .removeClass("desc");


  return;
},  


Then we traverse the sort array. If we find a match for the column name, we set currentIndex to the value of i, which is the matching element. This means that the current column being clicked on, is in the sort array.
toggleSort: function(col, id)
{
  var currentIndex = -1;
  $("#" + id)
  .removeClass("asc")
  .removeClass("desc");

  this.sort.forEach(
    (x, i) =>
    {
      if (x.col == col)
      {
        currentIndex = i;
      }
    }
  );


  return;
},  


Now, we have an If block to check if the current column name is represented in the sort array. If currentIndex is still -1 at this point, then it's not.
toggleSort: function(col, id)
{
  var currentIndex = -1;
  $("#" + id)
  .removeClass("asc")
  .removeClass("desc");

  this.sort.forEach(
    (x, i) =>
    {
      if (x.col == col)
      {
        currentIndex = i;
      }
    }
  );

  if (currentIndex > -1)
  {

  }
  else
  {

  }

  
  return;
},  


In that case, we'll push a new object into sort, with the current column's name and a default dir property value of "asc". We'll amend the current button's class accordingly as well.
toggleSort: function(col, id)
{
  var currentIndex = -1;
  $("#" + id)
  .removeClass("asc")
  .removeClass("desc");

  this.sort.forEach(
    (x, i) =>
    {
      if (x.col == col)
      {
        currentIndex = i;
      }
    }
  );

  if (currentIndex > -1)
  {

  }
  else
  {
    this.sort.push({ "col": col, "dir": "asc"});
    $("#" + id).addClass("asc");

  }
  
  return;
},  


Now, if the current column is already in sort, we'll want to change its direction. We first check if the current dir property value is "asc".
toggleSort: function(col, id)
{
  var currentIndex = -1;
  $("#" + id)
  .removeClass("asc")
  .removeClass("desc");

  this.sort.forEach(
    (x, i) =>
    {
      if (x.col == col)
      {
        currentIndex = i;
      }
    }
  );

  if (currentIndex > -1)
  {
    if (this.sort[currentIndex].dir == "asc")
    {

    }
    else
    {

    }

  }
  else
  {
    this.sort.push({ "col": col, "dir": "asc"});
    $("#" + id).addClass("asc");
  }
  
  return;
},  


If so, we change it to "desc" and amend the CSS class accordingly. If it's not "asc", then it can only be "desc". And if it's already "desc", we remove the entire object from the sort array accordingly, using the splice() method with currentIndex passed in. And finally, at the end of all that, we run filterRecords().
toggleSort: function(col, id)
{
  var currentIndex = -1;
  $("#" + id)
  .removeClass("asc")
  .removeClass("desc");

  this.sort.forEach(
    (x, i) =>
    {
      if (x.col == col)
      {
        currentIndex = i;
      }
    }
  );

  if (currentIndex > -1)
  {
    if (this.sort[currentIndex].dir == "asc")
    {
      this.sort[currentIndex].dir = "desc";
      $("#" + id).addClass("desc");

    }
    else
    {
      this.sort.splice(currentIndex, 1);
    }
  }
  else
  {
    this.sort.push({ "col": col, "dir": "asc"});
    $("#" + id).addClass("asc");
  }
  
  this.filterRecords();
  return;
},  


Refresh. Reduce page size to 20 for better clarity. Click on the column "Borrowed". The down-pointing triangle should disappear, and now the dataset is sorted only by "Title".


Click on "Title". Now it sorts descending.


Click on "Category". Now you see it that first sorts by "Title", then it sorts by "Category".


Click on "Title" twice, till the triangle disappears. Now it's just sorted by "Category". Click on "Title" again, so now it's sorted by "Category" first, then "Title". What a difference!


Have fun clicking on the other headers. You can sort by as many columns as you want.

Next

Searching, and getting different years' records.