<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>johna's blog</title>
<link>https://johna.compoutpost.com/</link>
<description>...mostly about web development and programming, with a little bit of anything else related to the Internet, computers and technology.</description>
<item>
<title>Another pointless project - the programmable digital watch</title>
<link>https://johna.compoutpost.com/blog/1346/another-pointless-project-the-programmable-digital-watch/</link>
<description>&lt;p&gt;&lt;img alt=&quot;Programmable digital watch project.jpg&quot; src=&quot;/blog/thumb/img1346_programmable-digital-watch-project_lg.jpg&quot; class=&quot;img-fluid&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I've come up with yet another pointless project.&lt;/p&gt;
&lt;p&gt;Recently, I've rekindled my interest in digital watches after having found my old calculator watch from the 1990s.&lt;/p&gt;
&lt;p&gt;With mobile phones, there's no longer much need for a watch, let alone a calculator watch, and I was trying to think of what features might make a watch more appealing in the current age.&lt;/p&gt;
&lt;p&gt;To be honest, I couldn't think of any new features or good reasons to wear a watch other than for fashion reasons, or for people who want to be able to check the time quicker and more convenient than reaching for their mobile phone.&lt;/p&gt;
&lt;p&gt;But you can still buy a new calculator watch in 2025, so that indicates to me that there is a market for pointless watches for hobbyists. So what would be of interest to hobbyists?&lt;/p&gt;
&lt;p&gt;The only thing that I could come up with was being able to program your own watch. I know you can get smart watches and have apps for any purpose, or create your own app, but I was thinking of a much simpler, more convention digital watch that uses a typical watch battery that lasts for years rather than needing to be charged every day.&lt;/p&gt;
&lt;p&gt;It would have a simple BASIC-style programming language that would have minimal features and commands but enough to do anything a watch needs to do and more.&lt;/p&gt;
&lt;p&gt;How would you program it? You could develop in a desktop application or browser-based editor with a built-in emulator, and then compile into some sort of minimalist opcode and transfer to the watch by a simple serial two-pin cable. No USB as that might add to the complexity of the computing power needed for the watch.&lt;/p&gt;
&lt;p&gt;As a proof-of-concept I started working on a web-based emulator with a JavaScript library of functions, similar to what I would expect the watch would have. For my POC, developing your own programs is in the JavaScript language simply because I didn't want to go to the lengths of creating my own compiler.&lt;/p&gt;
&lt;p&gt;I have no idea the available computing power of a digital watch so I don't know if this is actually possible. For the POC, I kept things pretty minimal. I chose a display consisting of 4 rows capable of alphanumeric characters plus a row of common icons (AM, PM, 24HR, DST, ALM, SNZ, CHIME). The font is made up of 5 x 5 pixels. However, a fully customisable pixel display would be more flexible if possible.&lt;/p&gt;
&lt;p&gt;The top and bottom rows are 13 characters long and are intended to indicate what each of the four buttons do, as their functions would not be printed on the watch, and will vary depending on the mode and settings of the watch.&lt;/p&gt;
&lt;p&gt;I liked the idea of the different modes of the clock being stored in self-contained modules so did this for my example.&lt;/p&gt;
&lt;p&gt;This is very much a work-in-progress. The JavaScript library is only for the purpose of emulating what the watch would do. &lt;/p&gt;
&lt;p&gt;It's been quite interesting thinking of how to make the various watch functions work and the potential problems associated with certain situations, such as if an alarm was to go off while in the middle of setting the time.&lt;/p&gt;
&lt;p&gt;If you are reading this post, my question to you is if you had access to a programmable digital watch, what features would you like to create?&lt;/p&gt;
&lt;div class=&quot;alert alert-info&quot;&gt;UPDATE: turns out there is already something similar to this out there. &lt;a href=&quot;https://www.sensorwatch.net/&quot; target=&quot;_blank&quot;&gt;Sensor Watch&lt;/a&gt; is a new circuit board that fits inside a Casio F-91W watch housing and has an ARM processor. There is various firmware to download and a framework for creating your own. Updates are through USB and despite being a powerful processor, the battery can last 18 months. As good as this is, and much more powerful than I imagined possible, the programming part is more complicated than I would have aimed for, and the F-91W display is very limiting.&lt;/div&gt;
&lt;div class=&quot;alert alert-info&quot;&gt;UPDATE 2: Back in 1984 there was the Seiko UC-2000 -- a digital watch with a docking station with keyboard and even a printer. These were programmable in Microsoft BASIC. Read about it on &lt;a href=&quot;https://hackaday.com/2024/07/16/seiko-had-a-smartwatch-in-1984/&quot; target=&quot;_blank&quot;&gt;Hackaday&lt;/a&gt; or find some interesting information &lt;a href=&quot;https://github.com/azya52/seiko&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt; including an emulator.&lt;br&gt;&lt;br&gt;&lt;img alt=&quot;Seiko UC-2000&quot; src=&quot;/blog/thumb/img1346_seiko-uc-2000_lg.jpg&quot; class=&quot;img-fluid&quot; /&gt;&lt;/div&gt;
&lt;h2&gt;Try it&lt;/h2&gt;
&lt;p&gt;Here is my incomplete first attempt at the proof-of-concept.&lt;/p&gt;
&lt;p&gt;The main clock mostly works, setting the date and time works but has issues due to problems with the JavaScript library, and the stopwatch works pretty well. I will keep working on these and the other functions like alarm, dual time and countdown timer.&lt;/p&gt;
&lt;p&gt;Inspect the page code to see how it works.&lt;/p&gt;
&lt;p&gt;Once the code is in a better state I might make a package that can be downloaded and will be easier to do some development for.&lt;/p&gt;
&lt;style type=&quot;text/css&quot;&gt;

        .pdw-container {
            background: url(/blog/uploads/img1346_watch-background.jpg) no-repeat;
            height: 681px;
            position: relative;
            width: 478px;
        }

        .pdw-display {
            position: absolute;
            left: 133px;
            top: 265px;
        }

        .pdw-container .button {
            height: 36px;
            position: absolute;
            /*outline: 1px solid red;*/
            width: 12px;
        }

        .pdw-container .button:hover {
            cursor: pointer;
            height: 36px;
            position: absolute;
            outline: 1px solid blue;
            width: 12px;
        }

        #button0 {
            left: 414px;
            top: 264px;
        }

        #button1 {
            left: 414px;
            top: 354px;
        }

        #button2 {
            left: 48px;
            top: 354px;
        }

        #button3 {
            left: 48px;
            top: 264px;
        }

        .r {
            display: flex;
            gap: 2px;
        }

        .r.md {
            gap: 2px;
        }

        .r.lg {
            gap: 2px;
        }

        .chr {
            display: flex;
            flex-wrap: wrap;
            gap: 1px;
            width: 14px;
        }

        .chr span {
            background-color: #eee;
            height: 2px;
            width: 2px;
        }

        .md .chr {
            width: 19px;
        }

        .md .chr span {
            height: 3px;
            width: 3px;
        }

        .lg .chr {
            width: 24px;
        }

        .lg .chr span {
            height: 8px;
            width: 4px;
        }

        .chr span.on {
            background-color: #111;
        }

        .dispr {
            font-family: monospace;
            font-size: 10px;
            color: #eee;
            display: flex;
            justify-content: space-between;
            width: 208px;
        }

        .dispr .on {
            color: #111;
        }

    &lt;/style&gt;

&lt;div class=&quot;pdw-container mb-4&quot;&gt;
    &lt;div class=&quot;pdw-display&quot;&gt;

        &lt;div class=&quot;r&quot;&gt;
            &lt;div id=&quot;r0c0&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r0c1&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r0c2&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r0c3&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r0c4&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r0c5&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r0c6&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r0c7&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r0c8&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r0c9&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r0c10&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r0c11&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r0c12&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;r md&quot; style=&quot;margin-top: 8px;&quot;&gt;
            &lt;div id=&quot;r1c0&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r1c1&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r1c2&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r1c3&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r1c4&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r1c5&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r1c6&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r1c7&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r1c8&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r1c9&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;dispr&quot; style=&quot;margin-top: 1px;&quot;&gt;
            &lt;span id=&quot;disp24hr&quot;&gt;24HR&lt;/span&gt;
            &lt;span id=&quot;dispam&quot;&gt;AM&lt;/span&gt;
            &lt;span id=&quot;disppm&quot;&gt;PM&lt;/span&gt;
            &lt;span id=&quot;dispdst&quot;&gt;DST&lt;/span&gt;
            &lt;span id=&quot;dispalarm&quot;&gt;ALM&lt;/span&gt;
            &lt;span id=&quot;dispsnooze&quot;&gt;SNZ&lt;/span&gt;
            &lt;span id=&quot;dispchime&quot;&gt;CHIME&lt;/span&gt;
        &lt;/div&gt;

        &lt;div class=&quot;r lg&quot; style=&quot;margin-top: 0px;&quot;&gt;
            &lt;div id=&quot;r2c0&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r2c1&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r2c2&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r2c3&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r2c4&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r2c5&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r2c6&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r2c7&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;r&quot; style=&quot;margin-top: 8px;&quot;&gt;
            &lt;div id=&quot;r3c0&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r3c1&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r3c2&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r3c3&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r3c4&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r3c5&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r3c6&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r3c7&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r3c8&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r3c9&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r3c10&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r3c11&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
            &lt;div id=&quot;r3c12&quot; class=&quot;chr&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
       
    &lt;/div&gt;
    &lt;a id=&quot;button0&quot; class=&quot;button button0&quot;&gt;&lt;/a&gt;
    &lt;a id=&quot;button1&quot; class=&quot;button button1&quot;&gt;&lt;/a&gt;
    &lt;a id=&quot;button2&quot; class=&quot;button button2&quot;&gt;&lt;/a&gt;
    &lt;a id=&quot;button3&quot; class=&quot;button button3&quot;&gt;&lt;/a&gt;
&lt;/div&gt;

&lt;script&gt;
//Constants

let chars = [];
//            0  0  0  0  0  1  1  1  1  1  2  2  2  2  2  3  3  3  3  3  4  4  4  4  4
chars['A'] = [0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1];
chars['B'] = [1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0];
chars['C'] = [0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1];
chars['D'] = [1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1 ,0];
chars['E'] = [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1];
chars['F'] = [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0];
chars['G'] = [0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1];
chars['H'] = [1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1];
chars['I'] = [0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0];
chars['J'] = [0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0];
chars['K'] = [1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1];
chars['L'] = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1];
chars['M'] = [1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1];
chars['N'] = [1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1];
chars['O'] = [0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0];
chars['P'] = [1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0];
chars['Q'] = [0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1];
chars['R'] = [1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1];
chars['S'] = [0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0];
chars['T'] = [1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0];
chars['U'] = [1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0];
chars['V'] = [1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0];
chars['W'] = [1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0];
chars['X'] = [1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1];
chars['Y'] = [1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0];
chars['Z'] = [1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1];
chars['0'] = [0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0];
chars['1'] = [0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0];
chars['2'] = [1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1];
chars['3'] = [1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0];
chars['4'] = [1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
chars['5'] = [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0];
chars['6'] = [0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0];
chars['7'] = [1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0];
chars['8'] = [0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0];
chars['9'] = [0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0];
chars[' '] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
chars[':'] = [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0];
chars['-'] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
chars['&gt;'] = [0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0];
chars['&lt;'] = [0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0];
chars['.'] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0];
chars[&quot;'&quot;] = [0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
chars['+'] = [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0];
//chars[''] = [];
//            0  0  0  0  0  1  1  1  1  1  2  2  2  2  2  3  3  3  3  3  4  4  4  4  4

//Variables

let tenths = 0;

//Button handlers
var button = [];
button[0] = null;
button[1] = null;
button[2] = null;
button[3] = null;

//On tick handlers
let ontick_list = [];

//Show time
let showtime_list = {};

//Running clocks
let clock_list = {};

/**
 * Clear the screen (row = null) or a row (0 to 3)
 * @param {*} row
 */
function clear(row = null) {
    if (row === 0 || row === null) {
        print(0, 0 , '             ');
    }
    if (row === 1 || row === null) {
        print(1, 0 , '          ');
    }
    if (row === 2 || row === null) {
        print(2, 0 , '        ');
    }
    if (row === 3 || row === null) {
        print(3, 0 , '             ');
    }
}

/**
 * Draw a single character at a row and column
 * @param {*} row
 * @param {*} col
 * @param {*} chr
 */
function drawChar(row, col, chr) {
    let content = '';
    chars[chr].forEach((pxl) =&gt; {
        if (pxl === 0) {
            content += '&lt;span&gt;&lt;/span&gt;';
        } else {
            content += '&lt;span class=&quot;on&quot;&gt;&lt;/span&gt;';
        }
    });

    document.getElementById(`r${row}c${col}`).innerHTML = content;
}

/**
 * Display one of the system icons:
 * 24hr, am, pm, alarm, dst or chime
 * @param {*} disp
 * @param {*} status
 */
function display(disp, status) {
    if (disp === '24hr') {
        if (status === 1) {
            document.getElementById('disp24hr').classList.add('on');
        } else {
            document.getElementById('disp24hr').classList.remove('on');
        }
    } else if (disp === 'am') {
        if (status === 1) {
            document.getElementById('dispam').classList.add('on');
        } else {
            document.getElementById('dispam').classList.remove('on');
        }
    } else if (disp === 'pm') {
        if (status === 1) {
            document.getElementById('disppm').classList.add('on');
        } else {
            document.getElementById('disppm').classList.remove('on');
        }
    } else if (disp === 'dst') {
        if (status === 1) {
            document.getElementById('dispdst').classList.add('on');
        } else {
            document.getElementById('dispdst').classList.remove('on');
        }
    } else if (disp === 'alarm') {
        if (status === 1) {
            document.getElementById('dispalarm').classList.add('on');
        } else {
            document.getElementById('dispalarm').classList.remove('on');
        }
    } else if (disp === 'snooze') {
        if (status === 1) {
            document.getElementById('dispsnooze').classList.add('on');
        } else {
            document.getElementById('dispsnooze').classList.remove('on');
        }
    } else if (disp === 'chime') {
        if (status === 1) {
            document.getElementById('dispchime').classList.add('on');
        } else {
            document.getElementById('dispchime').classList.remove('on');
        }
    }
}

/**
 * Print a string on a row starting at a column
 * @param {*} row
 * @param {*} col
 * @param {*} str
 */
function print(row, col, str) {
    for (var i = 0; i &lt; str.length; i++) {
        drawChar(row, col + i, str.substring(i, i + 1));
    }
}

/**
 * Assign a handler to a button
 * 0 = TR, 1 = BR, 2 = BL, 3 = TL
 * @param {*} btn
 * @param {*} fn
 */
function onbutton(btn, fn) {
    button[btn] = fn;
}

/**
 * Call a handler every second
 * @param {*} name
 * @param {*} fn
 */
function ontick(name, fn) {
    onticks[name] = fn;
}

/**
 * Set optional date parts
 * @param {*} obj
 * @param {*} y
 * @param {*} m
 * @param {*} d
 */
function setdate(obj, y, m, d) {
    if (y !== null) {
        obj.setFullYear(y);
    }

    if (m !== null) {
        obj.setMonth(m);
    }

    if (d !== null) {
        obj.setDate(d);
    }
}

function addshowdate(name, obj, row, col, format) {
    print(row, col, formatDateOrTime(obj, format));
    showtime_list[name] = [obj, row, col, format, 0];
}

function removeshowdate(name) {
    showtime_list[name] = null;
}

function addshowtime(name, obj, row, col, format, ampm24hr) {
    printtime(obj, row, col, format, ampm24hr);
    showtime_list[name] = [obj, row, col, format, ampm24hr];
}

function removeshowtime(name) {
    if (!showtime_list[name]) {
        return;
    }
    print(showtime_list[name][1], showtime_list[name][2], (' ').repeat(showtime_list[name][3].length));
    showtime_list[name] = null;
}

function printtime(obj, row, col, format, ampm24hr) {
    if (ampm24hr === 1) {
        if (obj.getHours() &gt; 11) {
            display('am', 0);
            display('pm', 1);
            } else {
            display('am', 1);
            display('pm', 0);
        }
        display('24hr', 0);
    } else if (ampm24hr === 2) {
        display('am', 0);
        display('pm', 0);
        display('24hr', 1);
    } else {
        display('am', 0);
        display('pm', 0);
        display('24hr', 0);
    }
    print(row, col, formatDateOrTime(obj, format, ampm24hr));
}

function formatDateOrTime(val, format, ampm24hr) {
    let groups = [];
    let count = 1;
    for (let i = 0; i &lt; format.length; i++) {
        if (format[i] === format[i + 1]) {
             count++;
        } else {
            groups.push(format[i].repeat(count));
            count = 1;
        }
     }
     
     let result = '';
     groups.forEach((fmt) =&gt; {
        //TODO need formats for leading 0 and leading space and flashing
        if (
            fmt === fmt.toUpperCase() &amp;&amp; tenths &gt;= 50
            &amp;&amp; (
                fmt === 'YY'
                || fmt === 'YYYY'
                || fmt === 'M'
                || fmt === 'MM'
                || fmt === 'MMM'
                || fmt === 'D'
                || fmt === 'DD'
                || fmt === 'WW'
                || fmt === 'WWW'
                || fmt === 'H'
                || fmt === 'HH'
                || fmt === 'N'
                || fmt === 'NN'
                || fmt === 'S'
                || fmt === 'SS'
                || fmt === 'F'
                || fmt === 'FF'
                || fmt === 'FFF'
            )
        ) {
            result += (' ').repeat(fmt.length);
        } else {
            fmt = fmt.toLowerCase();
            if (fmt === 'yy') {
                result += ('' + val.getFullYear()).slice(-2);
            } else if (fmt === 'yyyy') {
                result += val.getFullYear();
            } else if (fmt === 'm') {
                result += (val.getMonth() + 1);
            } else if (fmt === 'mm') {
                result += ('0' + (val.getMonth() + 1)).slice(-2);
            } else if (fmt === 'mmm') {
                switch (val.getMonth()) {
                    case 0:
                        result += 'JAN';
                        break;
                    case 1:
                        result += 'FEB';
                        break;
                    case 2:
                        result += 'MAR';
                        break;
                    case 3:
                        result += 'APR';
                        break;
                    case 4:
                        result += 'MAY';
                        break;
                    case 5:
                        result += 'JUN';
                        break;
                    case 6:
                        result += 'JUL';
                        break;
                    case 7:
                        result += 'AUG';
                        break;
                    case 8:
                        result += 'SEP';
                        break;
                    case 9:
                        result += 'OCT';
                        break;
                    case 10:
                        result += 'NOV';
                        break;
                    case 11:
                        result += 'DEC';
                }                                                                                                                                                                                                                        
            } else if (fmt === 'd') {
                result += val.getDate();
            } else if (fmt === 'dd') {
                result += ('0' + val.getDate()).slice(-2);
            } else if (fmt === 'ww') {
                switch (val.getDay()) {
                    case 0:
                        result += 'SU';
                        break;
                    case 1:
                        result += 'MO';
                        break;
                    case 2:
                        result += 'TU';
                        break;
                    case 3:
                        result += 'WE';
                        break;
                    case 4:
                        result += 'TH';
                        break;
                    case 5:
                        result += 'FR';
                        break;
                    case 6:
                        result += 'SA';
                        break;
                }
            } else if (fmt === 'www') {
                switch (val.getDay()) {
                    case 0:
                        result += 'SUN';
                        break;
                    case 1:
                        result += 'MON';
                        break;
                    case 2:
                        result += 'TUE';
                        break;
                    case 3:
                        result += 'WED';
                        break;
                    case 4:
                        result += 'THU';
                        break;
                    case 5:
                        result += 'FRI';
                        break;
                    case 6:
                        result += 'SAT';
                        break;
                }
            } else if (fmt === 'h') {
                let h = val.getHours();
                if (ampm24hr !== 2) {
                    if (h === 0) {
                        h = 12;
                    } else if (h &gt; 12) {
                        h = h - 12;
                    }
                }
                result += h;
            } else if (fmt === 'hh') {
                let h = val.getHours();
                if (ampm24hr !== 2) {
                    if (h === 0) {
                        h = 12;
                    } else if (h &gt; 12) {
                        h = h - 12;
                    }
                }
                result += ('0' + h).slice(-2);
            } else if (fmt === 'n') {
                result += val.getMinutes();
            } else if (fmt === 'nn') {
                result += ('0' + val.getMinutes()).slice(-2);
            } else if (fmt === 's') {
                result += val.getSeconds();
            } else if (fmt === 'ss') {
                result += ('0' + val.getSeconds()).slice(-2);
            } else if (fmt === 'f') {
                let f = ('00' + val.getMilliseconds()).slice(-3);
                result += (f).substring(0, 1);
            } else if (fmt === 'ff') {
                let f = ('00' + val.getMilliseconds()).slice(-3);
                result += (f).substring(0, 2);
            } else if (fmt === 'fff') {
                let f = ('00' + val.getMilliseconds()).slice(-3);
                result += (f).substring(0, 3); //Unsupported - only shows hundredths
            } else {
                result += fmt;
            }
        }
     });


    return result;
}

/**
 * Set optional time parts
 * @param {*} obj
 * @param {*} h
 * @param {*} m
 * @param {*} s
 * @param {*} ms
 */
function settime(obj, h, m, s, ms) {
    //TODO something here broken - results in invalid date
    if (h !== null) {
        obj.setHours(h);
    }

    if (m !== null) {
        obj.setMinutes(m);
    }

    if (s !== null) {
        obj.setSeconds(s);
    }

    if (ms !== null) {
        obj.setMilliseconds(ms);
    }
}

//System on tick handler
function system_ontick() {
    if (tenths &lt; 99) {
        tenths++;
    } else {
        tenths = 0;
    }

    for (var obj in clock_list) {
        if (clock_list[obj] !== null &amp;&amp; clock_list[obj][1] !== 0) {
            //clock_list[obj][0].setSeconds(clock_list[obj][0].getSeconds() + clock_list[obj][1]);
            clock_list[obj][0].setMilliseconds(clock_list[obj][0].getMilliseconds() + (clock_list[obj][1] * 10));
        }
    }

    for (var obj in showtime_list) {
        if (showtime_list[obj] !== null) {
            printtime(showtime_list[obj][0], showtime_list[obj][1], showtime_list[obj][2], showtime_list[obj][3], showtime_list[obj][4]);
        }
    }
}

function addclock(name, obj, inc) {
    clock_list[name] = [obj, inc];
    //clock_list.push([obj, inc]);
}

function removeclock(name) {
    clock_list[name] = null;
    //clock_list.push([obj, inc]);
}

function button0_click() {
    if (button[0] !== null) {
        button[0]();
    }
}

function button1_click() {
    if (button[1] !== null) {
        button[1]();
    }
}

function button2_click() {
    if (button[2] !== null) {
        button[2]();
    }
}

function button3_click() {
    if (button[3] !== null) {
        button[3]();
    }
}


//Initialise

clear();

document.getElementById('button0').addEventListener('click', button0_click);
document.getElementById('button1').addEventListener('click', button1_click);
document.getElementById('button2').addEventListener('click', button2_click);
document.getElementById('button3').addEventListener('click', button3_click);


setInterval(system_ontick, 10);

&lt;/script&gt;
&lt;script&gt;

var clock_mode = (function() {
    let clocktime = new Date(); //1970, 1, 1, 0, 0, 0, 0); //main time and date
    let clockmode = 0; //0 = am/pm, 1 = 24 hour clock
    let clockdst = 0; //0 DST off, 1 = DST on
    let clockset = 0; // 0 = not in set mode, 1...6 (s, h, m, y, m, d)

    function setclockdst() {
        if (clockdst === 0) {
            clocktime.setHours(clocktime.getHours() + 1);
            clockdst = 1;
        } else {
            clocktime.setHours(clocktime.getHours() - 1);
            clockdst = 0;
        }
        display('dst', clockdst);
        //update the time immediately
        showtime();
    }
    
    function setclockmode() {
        if (clockmode === 0) {
            clockmode = 1;
        } else {
            clockmode = 0;
        }
        //apply the 24 hours to the clock setting
        showtime();
    }

    function showtime() {
        addshowdate('clock', clocktime, 1, 0, 'www dd mmm');
        // if (clockmode === 0) {
        //     addshowtime('clock', clocktime, 2, 0, 'hh:nn ss', 1); //0 = dont show am/pm/24hr, 1 = show am/pm, 2 = show 24hr
        // } else {
        //     addshowtime('clock', clocktime, 2, 0, 'hh:nn ss', 2);
        // }
        addshowtime('clock', clocktime, 2, 0, 'hh:nn ss', clockmode + 1);
    }

    function showclock() {
        print(0, 9, 'DST&gt;');
        print(3, 8, '24HR&gt;');
        print(0, 0, '&lt;SET ');
        showtime();
        onbutton(0, setclockdst);
        onbutton(1, setclockmode);
        onbutton(3, setclock);
    }

    //Clock setting

    function setclock() {
        if (clockset === 0) {
            print(0, 9, 'INC&gt;');
            print(3, 8, 'NEXT&gt;');
            print(0, 0, '&lt;DONE');

            onbutton(0, setclockinc);
            onbutton(1, setclockadv);
    
            setclockadv();
        } else {
            showclock();

            clockset = 0;
        }
    }
    
    function setclockadv() {
        if (clockset === 6) {
            clockset = 1;
        } else {
            clockset = clockset + 1;
        }
        showsetclock();
    }
    
    function showsetclock() {
        if (clockset &lt;= 3) {
            addshowdate('clock', clocktime, 1, 0, 'www dd mmm');
            switch (clockset) {
                case 1:
                    addshowtime('clock', clocktime, 2, 0, 'hh:nn SS', clockmode + 1);
                    addshowtime('clock', clocktime, 2, 0, 'hh:nn SS', clockmode + 1);
                    break;
                case 2:
                    addshowtime('clock', clocktime, 2, 0, 'HH:nn ss', clockmode + 1);
                    break;
                case 3:
                    addshowtime('clock', clocktime, 2, 0, 'hh:NN ss', clockmode + 1);
                    break;
            }
        } else {
            addshowtime('clock', clocktime, 2, 0, 'hh:nn ss', clockmode + 1);
            switch (clockset) {
                case 4:
                    addshowdate('clock', clocktime, 1, 0, 'www DD mmm');
                    break;
                case 5:
                    addshowdate('clock', clocktime, 1, 0, 'www dd MMM');
                    break;
                case 6:
                    addshowdate('clock', clocktime, 1, 0, &quot;'YY dd mmm&quot;);
                    break;
            }
        }
    }

    function setclockinc() {
        if (clockset === 1) {
            //seconds - set to zero
            clocktime.setSeconds(0);
        } else if (clockset === 2) {
            //hours
            if (clocktime.getHours() === 23) {
                clocktime.setHours(0);
            } else {
                clocktime.setHours(clocktime.getHours() + 1);
            }
        } else if (clockset === 3) {
            //minutes
            if (clocktime.getMinutes() === 59) {
                clocktime.setMinutes(0);
            } else {
                clocktime.setMinutes(clocktime.getMinutes() + 1);
            }
        } else if (clockset === 4) {
            //year
            if (clocktime.getFullYear() === 2125) {
                clocktime.setFullYear(2025);
            } else {
                clocktime.setFullYear(clocktime.getFullYear() + 1);
            }
        } else if (clockset === 5) {
            //month
            if (clocktime.getMonth() === 11) {
                clocktime.setMonth(0);
            } else {
                clocktime.setMonth(clocktime.getMonth() + 1);
            }
        } else if (clockset === 6) {
            //day
            //if (clocktime.getDate()) ldom
        }
    }

    //end clock setting

    let api = {};

    api.init = function() {
        setdate(clocktime, 2025, 0, 23);
        //settime(clocktime, 0, 0, 0);
        //console.log(clocktime);
        addclock('clock', clocktime, 1);
    }

    api.show = function() {
        clear();
        // print(0, 0, '&lt;SET');
        //print(0, 9, 'DST&gt;');
        print(3, 0, '&lt;MODE');
        //print(3, 8, '24HR&gt;');
        // addshowdate('clock', clocktime, 1, 0, 'DDD dd MMM');
        // if (clockmode === 0) {
        //     addshowtime('clock', clocktime, 2, 0, 'hh:nn ss', 1); //0 = dont show am/pm/24hr, 1 = show am/pm, 2 = show 24hr
        // } else {
        //     addshowtime('clock', clocktime, 2, 0, 'hh:nn ss', 2);
        // }
        //showtime();
        //onbutton(0, setclockdst);
        //onbutton(1, setclockmode);
        //onbutton(3, setclock);
        showclock();
    }

    api.hide = function() {
        //kill showdate
        //kill showtime
        removeshowtime('clock');
        //clear some things
        display('am', 0);
        display('pm', 0);
        display('24hr', 0);
        display('dst', 0);

        onbutton(0, null);
        onbutton(1, null);
        onbutton(3, null);
    }

    api.getclocktime = function() {
        return clocktime;
    }

    api.getclockmode = function() {
        return clockmode;
    }

    return api;
})();

var alarm_mode = (function() {

    let alarmtime = new Date(1970, 1, 1, 0, 0, 0, 0); //alarm time only use time
    let alarmset = 0;
    let alarmstatus = 0; //0 = alarm off, 1 = alarm on
    let snoozestatus = 0; //0 = alarm off, 1 = alarm on
    let chimestatus = 0; //hourly chime 0 = off, 1 = on

    function showstatus() {
        display('alarm', alarmstatus);
        display('snooze', snoozestatus);
        display('chime', chimestatus);
    }

    function showtime() {
        addshowtime('alarmtime', alarmtime, 2, 0, 'hh:nn', 0);
    }

    function showalarm() {
        print(0, 7, 'CHIME&gt;');
        print(3, 7, 'ALARM&gt;');
        print(0, 0, '&lt;SET ');
        showstatus();
        showtime();
        onbutton(0, setalarmchime);
        onbutton(1, setalarmstatus);
        onbutton(3, setalarm);
    }

    function setalarmchime() {
        if (chimestatus === 0) {
            chimestatus = 1;
        } else {
            chimestatus = 0;
        }
        showstatus();
    }

    function setalarmstatus() {
        if (alarmstatus === 0) {
            alarmstatus = 1;
        } else {
            alarmstatus = 0;
        }
        showstatus();
    }

    //Clock setting

    function showsetalarm() {
        switch (alarmset) {
            case 1:
                addshowtime('alarmtime', alarmtime, 2, 0, 'HH:nn', clock_mode.getclockmode());
                break;
            case 2:
                addshowtime('alarmtime', alarmtime, 2, 0, 'hh:NN', clock_mode.getclockmode());
                break;
        }
    }

    function setalarm() {
        if (alarmset === 0) {
            print(0, 7, '  INC&gt;');
            print(3, 7, ' NEXT&gt;');
            print(0, 0, '&lt;DONE');

            onbutton(0, setalarminc);
            onbutton(1, setalarmadv);
    
            snoozestatus = 0; //might need to be redisplayed

            setalarmadv();
        } else {
            alarmstatus = 1;

            showalarm();

            alarmset = 0;
        }
    }

    function setalarmadv() {
        if (alarmset === 2) {
            alarmset = 1;
        } else {
            alarmset = alarmset + 1;
        }
        showsetalarm();
    }

    function setalarminc() {
        if (alarmset === 1) {
            //hours
            if (alarmtime.getHours() === 23) {
                alarmtime.setHours(0);
            } else {
                alarmtime.setHours(alarmtime.getHours() + 1);
            }
        } else if (alarmset === 2) {
            //minutes
            if (alarmtime.getMinutes() === 59) {
                alarmtime.setMinutes(0);
            } else {
                alarmtime.setMinutes(alarmtime.getMinutes() + 1);
            }
        }
    }

    let api = {};

    api.init = function () {
        //show alarm status as stays on screen at all time
        showstatus();
    };

    api.show = function() {
        clear();
        print(3, 0, '&lt;MODE');

        print(1, 0, 'ALM');
        //Show time
        addshowtime('alarmclock', clock_mode.getclocktime(), 1, 5, 'hh:nn', clock_mode.getclockmode());

        showalarm();
    }

    api.hide = function() {
        
        if (alarmstatus !== 0) {
            setalarm();
        }

        removeshowtime('alarmclock');

        removeshowtime('alarmtime');
        //clear some things
        display('am', 0);
        display('pm', 0);
        display('24hr', 0);

        onbutton(0, null);
        onbutton(1, null);
        onbutton(3, null);

    }

    return api;
})();

var stopwatch_mode = (function() {

    let stopwatchtime = new Date(1970, 1, 1, 0, 0, 0, 0); //stopwatch (time only)
    let stopwatchstatus = 0; //0 = stopwatch stopped, 1 = stopwatch running
    let splittime = new Date(1970, 1, 1, 0, 0, 0, 0); //lap time (time only)
    let splitstatus = 0; //0 = lap in active, 1 = lap active

    function reset() {
        if (stopwatchstatus === 0) {
            //reset it
            stopwatchtime = new Date(1970, 1, 1, 0, 0, 0, 0);
            showtime();
            splitstatus = 0;
            removeshowtime('stopwatchsplit');
        } else {
            if (splitstatus === 0) {
                splittime = new Date(stopwatchtime.getTime());
                splitstatus = 1;
                addshowtime('stopwatchsplit', splittime, 1, 0, 'nn:ss.ff', 0);
            } else {
                splitstatus = 0;
                removeshowtime('stopwatchsplit');
            }
        }
    }

    function changestatus() {
        if (stopwatchstatus === 0) {
            stopwatchstatus = 1;
        } else {
            stopwatchstatus = 0;
        }
        addclock('stopwatch', stopwatchtime, stopwatchstatus);
        showtime();
    }

    function showstatus() {
        if (stopwatchstatus === 0) {
            print(0, 7, 'START&gt;');
            print(3, 7, 'RESET&gt;');
        } else {
            print(0, 7, ' STOP&gt;');
            print(3, 7, 'SPLIT&gt;');
        }
    }

    function showtime() {
        addshowtime('stopwatch', stopwatchtime, 2, 0, 'nn:ss.ff', 0);
        //addshowtime('stopwatchms', stopwatchtime, 1, 8, 'ff', 0);
        showstatus();
    }

    let api = {};

    api.init = function() {
        addclock('stopwatch', stopwatchtime, 0);
    }

    api.show = function() {
        clear();
        print(3, 0, '&lt;MODE');
        //print(0, 7, 'SPLIT&gt;');
        //print(3, 8, 'START OR STOP&gt;');
        // addshowdate('clock', clocktime, 1, 0, 'DDD dd MMM');
        // if (clockmode === 0) {
        //     addshowtime('clock', clocktime, 2, 0, 'hh:nn ss', 1); //0 = dont show am/pm/24hr, 1 = show am/pm, 2 = show 24hr
        // } else {
        //     addshowtime('clock', clocktime, 2, 0, 'hh:nn ss', 2);
        // }
        //showstatus();
        showtime();
        onbutton(0, changestatus);
        onbutton(1, reset);
    }

    api.hide = function() {
        removeshowtime('stopwatch');
        //removeshowtime('stopwatchms');
    }

    return api;
})();

var timer_mode = (function() {

    let timertime = new Date(1970, 1, 1, 0, 0, 0, 0); //stopwatch (time only)
    let timerstatus = 0; //0 = stopwatch stopped, 1 = stopwatch running

    let api = {};

    api.init = function() {

    }

    api.show = function() {
        clear();
        print(3, 0, '&lt;MODE');
        print(1, 0, 'TIMER');
        print(2, 0, 'TODO');
        //showtime();
        //onbutton(0, changestatus);
        //onbutton(1, reset);
    }

    api.hide = function() {
        removeshowtime('timer');
    }

    return api;
})();

var dualtime_mode = (function() {

    let dualtime = new Date(1970, 1, 1, 0, 0, 0, 0); //stopwatch (time only)
    let dualdst = 0; //0 DST off, 1 = DST on

    let api = {};

    api.init = function() {

    }

    api.show = function() {
        clear();
        print(3, 0, '&lt;MODE');
        print(1, 0, 'DUALTIME');
        print(2, 0, 'TODO');
    }

    api.hide = function() {
        
    }

    return api;
})();










let mode = -1; //0 = clock, 1 = alarm, 2 = stopwatch, 3 = timer, 4 = dual

//Initialise
clock_mode.init();
alarm_mode.init();
//etc

onbutton(2, changemode);

changemode();

function changemode() {

    switch (mode) {
        case 0:
            clock_mode.hide();
            break;
        case 1:
            alarm_mode.hide();
            break;
        case 2:
            stopwatch_mode.hide();
            break;
        case 3:
            timer_mode.hide();
            break;
        case 4:
            dualtime_mode.hide();
            break;
    }

    if (mode &lt; 4) {
        mode++;
    } else {
        mode = 0;
    }

    switch (mode) {
        case 0:
            clock_mode.show();
            break;
        case 1:
            alarm_mode.show();
            break;
        case 2:
            stopwatch_mode.show();
            break;
        case 3:
            timer_mode.show();
            break;
        case 4:
            dualtime_mode.show();
            break;
    }
}

&lt;/script&gt;


&lt;h2&gt;Proposed/Example Command reference&lt;/h2&gt;
&lt;p&gt;LET / CONST / VAR&lt;br&gt;
Use these JavaScript commands to declare variables&lt;/p&gt;
&lt;p&gt;ADDSHOWDATE(name, datetimevalue, row, column, format)&lt;br&gt;
Shows a date on the specified row, starting at the specified column, in the specified format.&lt;/p&gt;
&lt;p&gt;ADDSHOWTIME(name, datetimevalue, row, column, format, clockmode)&lt;br&gt;
Shows a time on the specified row, starting at the specified column, in the specified format, with am/pm/24 hour indicator, and update when the time or date changes.&lt;br&gt;
Formatting string allows you to control what parts of dates and times are shown, how many digits, and whether they should flash or not (useful for a set mode).&lt;/p&gt;
&lt;p&gt;REMOVESHOWTIME(name)&lt;br&gt;
Stop showing and updating a time previously set with SHOWTIME.&lt;/p&gt;
&lt;p&gt;PRINT(row, col, stringvalue)&lt;br&gt;
Prints alphanumeric characters on the specified row starting at the specified column.&lt;/p&gt;
&lt;p&gt;ONBUTTON(buttonnum, handler)&lt;br&gt;
When a button is pressed (numbered 0 to 3 clockwise) call the specified handler function.&lt;/p&gt;
&lt;p&gt;ONTICK(name, handler)&lt;br&gt;
Add an event handler function that will be called every second. Set handler to NULL to stop event handling.&lt;/p&gt;
&lt;p&gt;ADDCLOCK(name, datetimevalue, increment)&lt;br&gt;
Add a datetime object that the watch will automatically increment based on the specific increment. The normal clock and stopwatch would typically increment by 1 second, a countdown timer would increment by -1 seconds.&lt;/p&gt;
&lt;p&gt;REMOVECLOCK(name)&lt;br&gt;
Remove a clock from being updated by the watch.&lt;/p&gt;
&lt;p&gt;CLEAR(row = NULL)&lt;br&gt;
Clear the screen or a specific row.&lt;/p&gt;
&lt;p&gt;DISPLAY(icon, status)&lt;br&gt;
Show or hide one of the built-in icons (am, pm, 24hr, dst, alarm, snooze, chime).&lt;/p&gt;
&lt;p&gt;DATE FUNCTIONS&lt;/p&gt;
&lt;p&gt;I added a couple of my own functions, but for this POC I am mostly using JavaScript date object functions.&lt;/p&gt;
&lt;p&gt;SETDATE()&lt;br&gt;
SETTIME()&lt;/p&gt;
&lt;p&gt;GETHOURS()&lt;br&gt;
GETMINUTES()&lt;br&gt;
GETSECONDS()&lt;br&gt;
GETMILLISECONDS()&lt;br&gt;
GETYEAR()&lt;br&gt;
GETMONTH()&lt;br&gt;
GETDATE()&lt;br&gt;
GETDAY()&lt;/p&gt;
&lt;p&gt;SETHOURS()&lt;br&gt;
SETMINUTES()&lt;br&gt;
SETSECONDS()&lt;br&gt;
SETMILLISECONDS()&lt;br&gt;
SETYEAR()&lt;br&gt;
SETMONTH()&lt;br&gt;
SETDATE()&lt;br&gt;
SETDAY()&lt;/p&gt;
&lt;h2&gt;TODO&lt;/h2&gt;
&lt;p&gt;I need to add a function to sound a chime or alarm, and possibly handle stopping an alarm rather than make the programmer handle this.&lt;/p&gt;
&lt;p&gt;It would be nice to be able to develop games for the watch so that would need the ability to display graphics, play sounds, and have an event handler quicker than every second.&lt;/p&gt;
</description>
<comments>https://johna.compoutpost.com/blog/1346/another-pointless-project-the-programmable-digital-watch/#comments</comments>
<pubDate>2025-01-20T12:00:00+10:00</pubDate>
<category>Electronics</category>
<category>Web Development</category>
<image>https://johna.compoutpost.com/blog/uploads/img1346_programmable-digital-watch-project.jpg</image>
<guid>https://johna.compoutpost.com/blog/1346</guid>
</item>
<item>
<title>Converting dBase IV programs to run in the browser and Text User Interfaces (TUI)</title>
<link>https://johna.compoutpost.com/blog/1333/converting-dbase-iv-programs-to-run-in-the-browser/</link>
<description>Recently, my mind drifted back 35 years ago. A time when I was developing business applications in dBase for DOS and Windows was only starting to become a thing.&lt;br&gt;&lt;br&gt;At the time my colleagues and I joked about business applications being developed for Windows. Imagine having to use a mouse to move around the screen and click on things instead of just using the keyboard to do everything?&lt;br&gt;&lt;br&gt;Time has, of course, proven that Windows is perfectly suitable for these types of applications but my reminiscence did prompt me to look at some of my old applications and think about whether there is still a place for text-based and keyboard-oriented applications. Are they better or faster to use for certain types of business applications?&lt;br&gt;&lt;br&gt;Then in a moment of insanity, I thought maybe the world might need or want a JavaScript framework for text-only, keyboard-oriented web applications.&lt;br&gt;&lt;br&gt;That lead me to wonder how would my old applications would run in the browser. One way to find out: try and convert one to run in JavaScript.&lt;br&gt;&lt;br&gt;So I created a HTML page with a fixed-width (monospace) font, created an element to house my 80x25 screen, and then copied across my old dBase program file to work on.&lt;br&gt;&lt;br&gt;I had to strip out all the database code as this was purely a test of the user interface and any database functionality would, of course, have to be done separately on a server.&lt;br&gt;&lt;br&gt;Converting dBase code to JavaScript is simple: I just had to change things the various dBase commands to the equivalent JavaScript. Like the WHILE, DO CASE and IF commands. The comparison operators = and # need some attention. String functions need to be changed. A lot of semi-colons and curly braces need to be added.&lt;br&gt;&lt;br&gt;But to make the dBase code work the same I needed to write some new functions. SAY prints some text at a specific row and column so I needed a way to do this. I opted for an array of strings for each row to act as the screen memory and this would be modified by the new say function and then the screen redrawn each time it changed.&lt;br&gt;&lt;br&gt;GET adds a control where data can be entered and needed some thought. I could use a HTML INPUT control or I could listen for keyboard input and write my own input control. I opted for the HTML INPUT control initially, mainly because it would be easier and quicker. So the GETs are now stored in an array and HTML controls inserted into the &quot;screen&quot; when the screen is redrawn.&lt;br&gt;&lt;br&gt;Now dBase IV is a procedural system where code execution waits for things like data entry and keypresses before it continues, but JavaScript is more event-driven and doesn't like to wait in loops for something to happen. To make it work I needed to add async functions and promises.&lt;br&gt;&lt;br&gt;I wrote a function equivalent of dBase's READ to handle form entry and added READKEY and LASTKEY equivalent functions to store the key(s) that were used to exit the form. I also converted the JavaScript key codes to the original dBase values.&lt;br&gt;&lt;br&gt;Various other functions added and changes made, and here is the result:&lt;br&gt;&lt;br&gt;&lt;div class=&quot;embed-responsive embed-responsive-4by3&quot;&gt;&lt;br&gt;  &lt;iframe class=&quot;embed-responsive-item&quot; src=&quot;/blog/uploads/att1333_invoice.htm&quot;&gt;&lt;/iframe&gt;&lt;br&gt;&lt;/div&gt;&lt;a href=&quot;/blog/uploads/att1333_invoice.htm&quot; target=&quot;_blank&quot;&gt;Open in new window&lt;/a&gt;&lt;br&gt;&lt;br&gt;It's a working application! And it actually works reasonably well except the data entry and key handling is not quite the same as dBase and has some issues. If I were to make a framework for these kind of applications I think it would be better to not use HTML controls and write my own. Or possibly embrace HTML form elements fully.&lt;br&gt;&lt;br&gt;Ideally any framework would be event-driven to better work with JavaScript.&lt;br&gt;&lt;br&gt;So are business applications better in a text and keyboard-orientated environment than in a GUI like Windows? If the application requires typing and you know what key does what then it can be quicker to enter data and complete a form. Typing into a normal web form with text inputs and selects can be a little slower as you alternate between keyboard and mouse, and most web forms have a button to submit the form, not a specific key, so you need to switch to the mouse or use the tab key to get to the submit button and then an extra keypress to operate the button using the keyboard.&lt;br&gt;&lt;br&gt;So I can kind of see a use for a framework that is focused on quick data entry using the keyboard. I have no idea if something like this already exists but will look in to it further.&lt;br&gt;&lt;br&gt;&lt;!--&lt;img alt=&quot;dBase IV invoice application in browser&quot; src=&quot;/blog/uploads/img1333_dbase-4-invoice-application-in-browser.JPG&quot; /&gt;--&gt;&lt;br&gt;&lt;br&gt;&lt;strong&gt;UPDATE:&lt;/strong&gt; I came across &lt;a href=&quot;https://github.com/vinibiavatti1/TuiCss&quot;&gt;TuiCss&lt;/a&gt;, a CSS and JavaScript library that can be used to make MS-DOS like applications, based on the Turbo Vision framework.&lt;br&gt;&lt;br&gt;&lt;a href=&quot;/blog/uploads/img1333_tuicss.jpg&quot;&gt;&lt;img alt=&quot;TuiCss screenshot&quot; src=&quot;/blog/uploads/img1333_tuicss.jpg&quot; class=&quot;img-fluid&quot; /&gt;&lt;/a&gt;</description>
<comments>https://johna.compoutpost.com/blog/1333/converting-dbase-iv-programs-to-run-in-the-browser/#comments</comments>
<pubDate>2024-09-13T12:00:00+10:00</pubDate>
<category>Web Development</category>
<category>Retro</category>
<image>https://johna.compoutpost.com/blog/uploads/img1333_dbase-4-invoice-application-in-browser.JPG</image>
<guid>https://johna.compoutpost.com/blog/1333</guid>
</item>
<item>
<title>How to set up a debugging using the Turnkey Linux LAMP stack and VS Code</title>
<link>https://johna.compoutpost.com/blog/1317/how-to-set-up-a-debugging-using-the-turnkey-linux-lamp-stack-and-vs-code/</link>
<description>In my &lt;a href=&quot;/blog/1314/how-to-set-up-a-website-and-database-using-the-turnkey-linux-lamp-stack/&quot;&gt;previous post&lt;/a&gt; I gave instructions on hosting a website and database on the Turnkey Linux LAMP stack. This post is a continuation and covers setting up debugging the website in Visual Studio Code.&lt;br&gt;&lt;br&gt;First step is to modify the php.ini file on the server. You can do this in Webmin. In the &lt;em&gt;System&lt;/em&gt; menu press &lt;em&gt;PHP Configuration&lt;/em&gt;. On the row for &quot;Configuration for mod_php&quot; press &lt;em&gt;Edit Manually&lt;/em&gt; to open the file for editing.&lt;br&gt;&lt;br&gt;&lt;div class=&quot;text-center&quot;&gt;&lt;a href=&quot;/blog/uploads/img1317_php-configuration.jpg&quot; target=&quot;_blank&quot;&gt;&lt;img alt=&quot;PHP Configuration&quot; src=&quot;/blog/thumb/img1317_php-configuration_sm.jpg&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br&gt;Then add a configuration section for Xdebug, as follows:&lt;br&gt;&lt;pre&gt;[xdebug]
xdebug.mode=develop,debug
xdebug.discover_client_host=1
xdebug.client_port = 9003
xdebug.start_with_request=yes&lt;/pre&gt;&lt;br&gt;After saving this change you will need to restart the Apache Webserver. This can be done in Webmin. Press &lt;em&gt;Apache Webserver&lt;/em&gt; in the &lt;em&gt;Servers&lt;/em&gt; menu and then press the &lt;em&gt;apply changes&lt;/em&gt; icon button near the top right corner.&lt;br&gt;&lt;br&gt;&lt;div class=&quot;text-center&quot;&gt;&lt;a href=&quot;/blog/uploads/img1317_edit-configuration-manually.jpg&quot; target=&quot;_blank&quot;&gt;&lt;img alt=&quot;Edit Configuration Manually.jpg&quot; src=&quot;/blog/thumb/img1317_edit-configuration-manually_sm.jpg&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br&gt;Then start VS Code and add the extensions &lt;em&gt;PHP Extension Pack&lt;/em&gt; by &lt;em&gt;Xdebug&lt;/em&gt; which includes &lt;em&gt;PHP Debug&lt;/em&gt; and &lt;em&gt;PHP IntelliSense&lt;/em&gt; and &lt;em&gt;SFTP&lt;/em&gt; by &lt;em&gt;Natizyskunk&lt;/em&gt;.&lt;br&gt;&lt;br&gt;You need PHP on your workstation for these extensions to work, so download it from &lt;a href=&quot;https://windows.php.net/download/&quot;&gt;windows.php.net&lt;/a&gt;. The LAMP stack currently includes PHP 7.4 so I downloaded &lt;a href=&quot;https://windows.php.net/downloads/releases/archives/php-7.4.32-nts-Win32-vc15-x64.zip&quot;&gt;PHP 7.4.32 NTS Windows 64-bit&lt;/a&gt;. Once downloaded and uncompressed move it to your preferred location, you then need to tell the extensions the path to php.exe with the following three settings in settings.json (use your own path, not mine and ensure php.exe is included):&lt;br&gt;&lt;br&gt;&lt;pre&gt;&quot;php.executablePath&quot;: &quot;D:\\Documents\\php-7.4.32-nts-Win32-vc15-x64\\php.exe&quot;,
&quot;php.debug.executablePath&quot;: &quot;D:\\Documents\\php-7.4.32-nts-Win32-vc15-x64\\php.exe&quot;,
&quot;php.validate.executablePath&quot;: &quot;D:\\Documents\\php-7.4.32-nts-Win32-vc15-x64\\php.exe&quot;&lt;/pre&gt;&lt;br&gt;&lt;div class=&quot;text-center&quot;&gt;&lt;a href=&quot;/blog/uploads/img1317_settings.jpg&quot; target=&quot;_blank&quot;&gt;&lt;img alt=&quot;settings.json&quot; src=&quot;/blog/thumb/img1317_settings_sm.jpg&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br&gt;You can now configure SFTP. Press Ctrl+Shift+P to bring up the command prompt and type &lt;code&gt;SFTP: config&lt;/code&gt;. You can then enter a configuration similar to the following, using your website details, and FTP user and password.&lt;br&gt;&lt;br&gt;&lt;pre&gt;{
    &quot;name&quot;: &quot;My Website&quot;,
    &quot;host&quot;: &quot;mywebsite.local&quot;,
    &quot;protocol&quot;: &quot;sftp&quot;,
    &quot;port&quot;: 22,
    &quot;username&quot;: &quot;mywebsite&quot;,
    &quot;password&quot;: &quot;abc123&quot;,
    &quot;remotePath&quot;: &quot;/var/mywebsite&quot;,
    &quot;uploadOnSave&quot;: false,
    &quot;useTempFile&quot;: false,
    &quot;openSsh&quot;: false
}&lt;/pre&gt;&lt;br&gt;&lt;div class=&quot;text-center&quot;&gt;&lt;a href=&quot;/blog/uploads/img1317_sftp-settings.jpg&quot; target=&quot;_blank&quot;&gt;&lt;img alt=&quot;sftp.json&quot; src=&quot;/blog/thumb/img1317_sftp-settings_sm.jpg&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br&gt;Next configure the debug settings.&lt;br&gt;&lt;br&gt;&lt;pre&gt;{
    &quot;name&quot;: &quot;Listen for Xdebug&quot;,
    &quot;type&quot;: &quot;php&quot;,
    &quot;request&quot;: &quot;launch&quot;,
    &quot;port&quot;: 9003,
    &quot;pathMappings&quot;: {
        &quot;/var/mywebsite&quot;: &quot;${workspaceFolder}/&quot;
    }
}&lt;/pre&gt;&lt;br&gt;&lt;div class=&quot;text-center&quot;&gt;&lt;a href=&quot;/blog/uploads/img1317_launch-settings.jpg&quot; target=&quot;_blank&quot;&gt;&lt;img alt=&quot;launch.json&quot; src=&quot;/blog/thumb/img1317_launch-settings_sm.jpg&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br&gt;&lt;br&gt;You are now ready to debug. Press F5 to start listening for Xdebug connections and set breakpoints where needed.&lt;br&gt;&lt;br&gt;If you need to set up URL rewriting, see my next post &lt;a href=&quot;/blog/1318/how-to-enable-url-rewriting-using-the-turnkey-linux-lamp-stack/&quot;&gt;How to enable URL rewriting using the Turnkey Linux LAMP stack&lt;/a&gt;.</description>
<comments>https://johna.compoutpost.com/blog/1317/how-to-set-up-a-debugging-using-the-turnkey-linux-lamp-stack-and-vs-code/#comments</comments>
<pubDate>2023-12-19T12:00:00+10:00</pubDate>
<category>Web Development</category>
<image>https://johna.compoutpost.com/blog/uploads/img1317_php-configuration.jpg</image>
<guid>https://johna.compoutpost.com/blog/1317</guid>
</item>
<item>
<title>How to set up a website and database using the Turnkey Linux LAMP stack</title>
<link>https://johna.compoutpost.com/blog/1314/how-to-set-up-a-website-and-database-using-the-turnkey-linux-lamp-stack/</link>
<description>If you need to host your own website for the purposes of web development, &lt;a href=&quot;https://www.turnkeylinux.org/lamp&quot; target=&quot;_blank&quot;&gt;Turnkey Linux LAMP Stack&lt;/a&gt; is an easy to install all-in-one solution that you can set up on a spare computer or a VM (Virtual Machine).&lt;br&gt;&lt;br&gt;I recently wanted to do this so I could locally test a website project I have been working on, and do PHP debugging. I had a spare old Celeron PC that I used for this purpose.&lt;br&gt;&lt;br&gt;Step one is to download the ISO and install it on a computer or VM. I used &lt;a href=&quot;https://rufus.ie/&quot; target=&quot;_blank&quot;&gt;Rufus&lt;/a&gt; to copy the ISO file to a USB drive but you don't need to do that if installing on a VM.&lt;br&gt;&lt;br&gt;Installation of Turnkey Linux is simple and the questions that get asked during guided to install should be easy to answer. Once installed you can log in with root and the password you chose during installation and then set up your network with either DHCP or a static IP. I chose a static IP so that I could set up a local domain name I could use when accessing my website locally.&lt;br&gt;&lt;br&gt;In Windows you can set up a local domain by editing the file C:WindowsSystem32driversetchosts. Note that you will need to run Notepad as an administrator to be able to save this file. Then just add a new line with the static IP address you chose and the domain name, for example:&lt;pre&gt;192.168.1.100 mywebsite.local&lt;/pre&gt;&lt;br&gt;For many of the next steps you will need to log in to Webmin, a built-in website that allows you to administer the server. The default address is https://192.168.1.100:12321 (change the IP address to the correct address for your server). You will need to bypass a security warning as the website uses an untrusted self-signed certificate. The user name is 'root' and the password is the one you chose during installation.&lt;br&gt;&lt;br&gt;Next step is to create a folder for your website. Go to the &lt;i&gt;Tools&lt;/i&gt; menu and then press &lt;i&gt;File Manager&lt;/i&gt;.&lt;br&gt;&lt;br&gt;The default websites are located in the &lt;i&gt;var&lt;/i&gt; folder, and I would suggest your website(s) also be created there, so select &lt;i&gt;Create new directory&lt;/i&gt; in the &lt;i&gt;File&lt;/i&gt; menu near the top right corner of the page, and type in a name for your website, eg &quot;mywebsite&quot;.&lt;br&gt;&lt;br&gt;&lt;div class=&quot;text-center&quot;&gt;&lt;a href=&quot;/blog/uploads/img1314_file-manager.jpg&quot; target=&quot;_blank&quot;&gt;&lt;img alt=&quot;Create website folder&quot; src=&quot;/blog/thumb/img1314_file-manager_sm.jpg&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br&gt;You can now go to the &lt;i&gt;Servers&lt;/i&gt; menu and press &lt;i&gt;Apache Webserver&lt;/i&gt;, then press &lt;i&gt;Create virtual host&lt;/i&gt;. There are many options for setting up a website, but I entered 80 for &lt;i&gt;Port&lt;/i&gt;, chose my newly created folder for &lt;i&gt;Document Root&lt;/i&gt;, and entered my local domain for &lt;i&gt;Server Name&lt;/i&gt;.&lt;br&gt;&lt;br&gt;&lt;div class=&quot;text-center&quot;&gt;&lt;a href=&quot;/blog/uploads/img1314_create-website.jpg&quot; target=&quot;_blank&quot;&gt;&lt;img alt=&quot;Create website&quot; src=&quot;/blog/thumb/img1314_create-website_sm.jpg&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br&gt;You can now navigate to your local domain in a web browser and you should get a &quot;Forbidden&quot; error message as you don't yet have any files or pages to serve.&lt;br&gt;&lt;br&gt;Most websites have a database, and the LAMP stack includes MariaDB, a MySQL compatible database server.&lt;br&gt;&lt;br&gt;We can now create a database and a database user.&lt;br&gt;&lt;br&gt;From the &lt;i&gt;Servers&lt;/i&gt; menu, press &lt;i&gt;MariaDB Database Server&lt;/i&gt;, and then press &lt;i&gt;Create new database&lt;/i&gt;. On the form enter a name for your database and optionally choose any other settings you may need, or just leave the defaults.&lt;br&gt;&lt;br&gt;&lt;div class=&quot;text-center&quot;&gt;&lt;a href=&quot;/blog/uploads/img1314_create-database.jpg&quot; target=&quot;_blank&quot;&gt;&lt;img alt=&quot;Create database&quot; src=&quot;/blog/thumb/img1314_create-database_sm.jpg&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br&gt;Once the database is created, press &lt;i&gt;User Permissions&lt;/i&gt; and then &lt;i&gt;Create new user&lt;/i&gt;. Enter a username and password and select &lt;i&gt;Permissions&lt;/i&gt; that your user will need (I selected all of them).&lt;br&gt;&lt;br&gt;&lt;div class=&quot;text-center&quot;&gt;&lt;a href=&quot;/blog/uploads/img1314_create-database-user.jpg&quot; target=&quot;_blank&quot;&gt;&lt;img alt=&quot;Create database user&quot; src=&quot;/blog/thumb/img1314_create-database-user_sm.jpg&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br&gt;After the user is created you can give the user access to your database. Press &lt;i&gt;Database Permissions&lt;/i&gt; and then &lt;i&gt;Create new database permissions&lt;/i&gt; and then select your new database and enter your new user name and then create the permission.&lt;br&gt;&lt;br&gt;&lt;div class=&quot;text-center&quot;&gt;&lt;a href=&quot;/blog/uploads/img1314_create-database-permissions.jpg&quot; target=&quot;_blank&quot;&gt;&lt;img alt=&quot;Create database permissions&quot; src=&quot;/blog/thumb/img1314_create-database-permissions_sm.jpg&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br&gt;You can check you have database access by going to https://192.168.1.100:12322 (change to your IP address) and logging in with your new database user. Once logged-in you can also import or create your database objects and data if you are ready.&lt;br&gt;Although you can manage files using the built-in file manager, you will most likely want FTP access so you can deploy code changes from your web development IDE. For this you will need to go to the &lt;i&gt;System&lt;/i&gt; menu and create a new user. Enter a username, select your website folder as the &lt;i&gt;Home directory&lt;/i&gt;, and enter a new password as the &lt;i&gt;Normal password&lt;/i&gt;.&lt;br&gt;&lt;br&gt;&lt;div class=&quot;text-center&quot;&gt;&lt;a href=&quot;/blog/uploads/img1314_create-user-for-ftp.jpg&quot; target=&quot;_blank&quot;&gt;&lt;img alt=&quot;Create user for FTP&quot; src=&quot;/blog/thumb/img1314_create-user-for-ftp_sm.jpg&quot; /&gt;&lt;/a&gt;&lt;/div&gt;&lt;br&gt;Once your user is created you can connect using SFTP to your server's IP address or local domain and using the user name and password you just created. You will need to change ownership to the folder to your FTP user and also change permissions.&lt;br&gt;&lt;br&gt;In a &lt;a href=&quot;/blog/1317/how-to-set-up-a-debugging-using-the-turnkey-linux-lamp-stack-and-vs-code/&quot;&gt;future post&lt;/a&gt;, I will document how to configure Xdebug and debug from Visual Studio and/or VS Code, and deployment via SFTP.</description>
<comments>https://johna.compoutpost.com/blog/1314/how-to-set-up-a-website-and-database-using-the-turnkey-linux-lamp-stack/#comments</comments>
<pubDate>2023-11-18T12:00:00+10:00</pubDate>
<category>Website Hosting</category>
<category>Web Development</category>
<image>https://johna.compoutpost.com/blog/uploads/img1314_file-manager.jpg</image>
<guid>https://johna.compoutpost.com/blog/1314</guid>
</item>
<item>
<title>Intermittent &quot;Unable to read data from the transport connection: net_io_connectionclosed&quot; errors</title>
<link>https://johna.compoutpost.com/blog/1175/intermittent-unable-to-read-data-from-the-transport-connection-net-io-connectionclosed-errors/</link>
<description>&lt;img alt=&quot;&quot; src=&quot;/blog/uploads/img1175_error-message.jpg&quot; class=&quot;img-fluid&quot; /&gt;&lt;br&gt;&lt;br&gt;I have been facing a problem with an ASP.NET application that sends email using a remote mail server.&lt;br&gt;&lt;br&gt;Some emails failed to send, and the error message logged was &quot;&lt;b&gt;Unable to read data from the transport connection: net_io_connectionclosed&lt;/b&gt;&quot;.&lt;br&gt;&lt;br&gt;The problems were very intermittent and started occurring after a change of mail server. It was not easy to reproduce the problem.&lt;br&gt;&lt;br&gt;I didn't get much help from the people supporting the new mail server, although they did confirm that the error was also logged at their end.&lt;br&gt;&lt;br&gt;I also spent plenty of time searching for possible causes and trying various solutions none of which worked.&lt;br&gt;&lt;br&gt;Sending was done using &lt;b&gt;System.Net.Mail&lt;/b&gt; library and there was some suggestion in my research that this has some issues and is actually deprecated (there is some confusion over that though). Microsoft is recommending using an open-source library called &lt;a href=&quot;https://github.com/jstedfast/MailKit&quot; target=&quot;_blank&quot;&gt;MailKit&lt;/a&gt; instead.&lt;br&gt;&lt;br&gt;So I decided to do some experimentation and rather than add any new dependencies I temporarily changed some of my code to use the outdated but believed to be more reliable &lt;b&gt;System.Web.Mail&lt;/b&gt; library. The problem went away &amp;ndash; no more errors!&lt;br&gt;&lt;br&gt;Unfortunately &lt;b&gt;System.Web.Mail&lt;/b&gt; couldn't easily do one thing I need to do (embedded images that don't also appear as attachments), so I opted to rewrite most of my mail sending code to use MailKit. MailKit can do all that &lt;b&gt;System.Net.Mail&lt;/b&gt; and &lt;b&gt;System.Web.Mail&lt;/b&gt; can do and more, and it's not hard to switch over to it.&lt;br&gt;&lt;br&gt;There's been no further email errors with MailKit so I would suggest that you move away from &lt;b&gt;System.Net.Mail&lt;/b&gt; if you ever face this error message.</description>
<comments>https://johna.compoutpost.com/blog/1175/intermittent-unable-to-read-data-from-the-transport-connection-net-io-connectionclosed-errors/#comments</comments>
<pubDate>2020-05-06T12:00:00+10:00</pubDate>
<category>Web Development</category>
<image>https://johna.compoutpost.com/blog/uploads/img1175_error-message.jpg</image>
<guid>https://johna.compoutpost.com/blog/1175</guid>
</item>
<item>
<title>500 Internal Server Error after migrating from IIS 7.5 to IIS 10</title>
<link>https://johna.compoutpost.com/blog/1114/500-internal-server-error-after-migrating-from-iis-7-5-to-iis-10/</link>
<description>&lt;img alt=&quot;iis10-hsts.jpg&quot; src=&quot;/blog/uploads/img1114_iis10-hsts.jpg&quot; class=&quot;img-fluid&quot; /&gt;&lt;br&gt;&lt;br&gt;As support ends for Microsoft Windows Server 2008 in January 2020, I have recently gone through migrating some websites to a new server running Windows Server 2016 and IIS 10.&lt;br&gt;&lt;br&gt;The websites were migrated with the Web Deploy Tool.&lt;br&gt;&lt;br&gt;Some of the websites did not work, and simply brought up a 500 Internal Server Error, bypassing custom error pages. This is always a sign that the cause of the error might be in the web.config file.&lt;br&gt;&lt;br&gt;After a little experimenting with commenting out certain sections of the web.config file, I found two sections that had compatibility issues between IIS 7.5 and IIS 10.&lt;br&gt;&lt;br&gt;The first was the &quot;staticContent&quot; section. I had used this to allow IIS to server .woff and .svg files. This is no longer needed for those types of files in IIS 10 so the section could be removed.&lt;br&gt;&lt;br&gt;The other problem was where the IIS URL rewrite module had been used to automatically redirect from HTTP to HTTPs.&lt;br&gt;&lt;br&gt;This module is not installed in IIS 10 by default.&lt;br&gt;&lt;br&gt;You could download the URL Rewrite extension (&lt;a href=&quot;https://www.iis.net/downloads/microsoft/url-rewrite&quot;&gt;https://www.iis.net/downloads/microsoft/url-rewrite&lt;/a&gt;) but if you are only using this to redirect from HTTP to HTTPS then IIS 10 has an easier way of doing this:&lt;br&gt;&lt;br&gt;&amp;bull; Open the IIS console and select the website&lt;br&gt;&amp;bull; On the &quot;Actions&quot; pane you will see the &quot;HSTS&quot; option below the &quot;Configure&quot; section&lt;br&gt;&amp;bull; Enable HSTS and &quot;Redirect Http to Https&quot;&lt;br&gt;&lt;br&gt;(Source: &lt;a href=&quot;https://www.hametbenoit.info/2018/10/22/windows-server-2019-you-can-now-easily-redirect-http-request-to-https/&quot;&gt;https://www.hametbenoit.info/2018/10/22/windows-server-2019-you-can-now-easily-redirect-http-request-to-https/&lt;/a&gt;)&lt;br&gt;&lt;br&gt;There may be other causes of these problems too but they should be reasonably easy to identify by commenting out sections of the web.config file one at a time and testing, and a little research with your favourite search engine.</description>
<comments>https://johna.compoutpost.com/blog/1114/500-internal-server-error-after-migrating-from-iis-7-5-to-iis-10/#comments</comments>
<pubDate>2019-11-04T12:00:00+10:00</pubDate>
<category>Computers & Internet</category>
<category>Web Development</category>
<image>https://johna.compoutpost.com/blog/uploads/img1114_iis10-hsts.jpg</image>
<guid>https://johna.compoutpost.com/blog/1114</guid>
</item>
<item>
<title>A jQuery plugin for a fixed table of contents</title>
<link>https://johna.compoutpost.com/blog/984/a-jquery-plugin-for-a-fixed-table-of-contents/</link>
<description>&lt;img alt=&quot;A jQuery plugin for a fixed table of contents&quot; src=&quot;/blog/uploads/img984_a-jquery-plugin-for-a-fixed-table-of-contents.jpg&quot; class=&quot;img-fluid&quot; /&gt;&lt;br&gt;&lt;br&gt;Some long web pages have a handy feature where a table of contents are shown beside the content which remains fixed.&lt;br&gt;&lt;br&gt;A good example of this was the Bootstrap 3.x &lt;a href=&quot;https://getbootstrap.com/docs/3.3/css/&quot; target=&quot;_blank&quot;&gt;documentation pages&lt;/a&gt;. Once you scroll down past a certain point the table of contents position changes to fixed so it is always visible.&lt;br&gt;&lt;br&gt;There is a problem with this particular implementation though. If the table of contents list is long and exceeds the height of the browser window, then it is impossible to see the bottom part of the table of contents.&lt;br&gt;&lt;br&gt;In Bootstrap 4.x (&lt;a href=&quot;http://getbootstrap.com/docs/4.1/components/card/&quot; target=&quot;_blank&quot;&gt;example&lt;/a&gt;) they changed the way tables of contents were displayed to avoid this problem. They are now always fixed position and if the content cannot fit in the vertical space they show a secondary scroll bar just for the table of contents.&lt;br&gt;&lt;br&gt;This works well enough, but I was looking for a more automatic method closer to the way it was done on the Bootstrap 3.x pages for my &lt;a href=&quot;http://bootstrapcustomizer.compoutpost.com/&quot; target=&quot;_blank&quot;&gt;Bootstrap Customizer&lt;/a&gt; website, but with the ability to be able to scroll to see any content that wouldn't fit in the browser window.&lt;br&gt;&lt;br&gt;This is quite a challenging task as the requirements are a little complex:&lt;br&gt;&lt;br&gt;1) The TOC (table of contents) displays and scrolls as normal until you scroll down to the end of the TOC. At this point the TOC displays as fixed with the bottom of TOC at the bottom of the browser window.&lt;br&gt;&lt;br&gt;2) When scrolled down to the end of the page, the TOC should not be displayed over the footer, and the end of the TOC should display at the end of it's container.&lt;br&gt;&lt;br&gt;3) When you scroll up the TOC should also scroll up. This should only happen up to the point where the top most item in the TOC is at the top of the screen, or has reached it's original natural position.&lt;br&gt;&lt;br&gt;Based on these requirements, I've been working on a prototype.&lt;br&gt;&lt;br&gt;&lt;pre&gt;(function ($) {
	$.fn.scrollfixed = function (options) {
		var settings = $.extend({
			breakpoint: 0
		}, options);

		var obj = this;
		var isFixed = false;
		var atBottom = false;
		var lastScrollTop = -1;
		var contents_top = obj.offset().top;

		var contentTop = 0;
		var contentTopOriginal = obj.offset().top;

		function handleScroll() {
			var contentHeight = obj.outerHeight(true);
			var containerTop = $('#' + options.container).offset().top;
			var containerHeight = $('#' + options.container).height();
			var containerBottom = containerTop + containerHeight;
			var viewportHeight = $(window).height();
			var scrollTop = $(window).scrollTop();

			var scrollingUp = scrollTop &lt; lastScrollTop;
			var scrollingDown = scrollTop &gt; lastScrollTop;

			var nextIsFixed;
			var nextContentTop;

			if (window.matchMedia('(min-width: ' + options.breakpoint + 'px)').matches &amp;&amp; containerHeight !== contentHeight) {

				if (scrollTop &gt; contentTopOriginal) {
					nextIsFixed = true;

					if (contentHeight &lt;= containerHeight &amp;&amp; (containerBottom - scrollTop - contentHeight) &lt; 0) {
						if ((containerBottom - scrollTop - contentHeight) &lt; 0) {
							nextContentTop = (containerBottom - scrollTop - contentHeight);
						}
						else {
							nextContentTop = 0;
						}
					}
					else {
						nextContentTop = contentTop;

						if (scrollingDown) {
							if (scrollTop &gt; containerBottom - (contentTop + contentHeight)) {
								nextContentTop += (lastScrollTop - scrollTop);
							}
							else if ((contentTop + contentHeight) &gt; viewportHeight) {
								nextContentTop += (lastScrollTop - scrollTop);
							}

							if (nextContentTop &lt; (viewportHeight - contentHeight)) {
								nextContentTop = (viewportHeight - contentHeight);
							}
						}
						else if (scrollingUp) {
							if (contentTop &lt; 0) {
								nextContentTop -= (scrollTop - lastScrollTop);
							}

							if (nextContentTop &gt; 0) {
								nextContentTop = 0;
							}
						}
					}
				}
				else {
					nextIsFixed = false;
					nextContentTop = 0;
					task = 'fits???';
				}
			}
			else {
				nextIsFixed = false;
				nextContentTop = 0;
			}

			if (nextIsFixed !== isFixed) {
				obj.css(&quot;position&quot;, (nextIsFixed === true ? &quot;fixed&quot; : &quot;&quot;));
				obj.css(&quot;left&quot;, (nextIsFixed === true ? obj.offset().left + &quot;px&quot; : &quot;&quot;));
			}

			if (nextContentTop !== contentTop || nextIsFixed !== isFixed) {
				obj.css(&quot;top&quot;, (nextIsFixed === true ? nextContentTop + &quot;px&quot; : &quot;&quot;));
			}

			isFixed = nextIsFixed;
			contentTop = nextContentTop;

			lastScrollTop = scrollTop;
		}

		$(window).scroll(function () {

			clearTimeout(handleScroll._tId);
			handleScroll._tId = setTimeout(function () {
				handleScroll();
			}, 10);
		});

		handleScroll();

		return this;
	};
}(jQuery));&lt;/pre&gt;&lt;br&gt;&lt;br&gt;This is done as a jQuery plugin, and can be called with parameters &quot;breakpoint&quot; (the pixel min-width at which the positional changes should take effect, so you can disable this functionality on small screens if needed) and &quot;container&quot; (the ID of the container object that should be used to calculate minimum and maximum positions - this would usually be something like a Bootstrap column).&lt;br&gt;&lt;br&gt;&lt;pre&gt;$(&quot;#myTOC&quot;).scrollfixed({ breakpoint: 768, container: 'myTOCContainer' });&lt;/pre&gt;&lt;br&gt;&lt;a href=&quot;/blog/uploads/att984_demo.html&quot; target=&quot;_blank&quot; class=&quot;btn btn-danger&quot;&gt;Demo&lt;/a&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;This achieves all of the objectives but there's still a few minor issues with this code which I need to improve.&lt;br&gt;&lt;br&gt;&lt;b&gt;Change History&lt;/b&gt;&lt;br&gt;&lt;br&gt;12-Dec-2020 - Code revised and improved, demo added&lt;br&gt;&lt;br&gt;</description>
<comments>https://johna.compoutpost.com/blog/984/a-jquery-plugin-for-a-fixed-table-of-contents/#comments</comments>
<pubDate>2018-10-12T12:00:00+10:00</pubDate>
<category>Web Development</category>
<image>https://johna.compoutpost.com/blog/uploads/img984_a-jquery-plugin-for-a-fixed-table-of-contents.jpg</image>
<guid>https://johna.compoutpost.com/blog/984</guid>
</item>
<item>
<title>How to add AJAX suggestions to yairEO's Tagify tag input component</title>
<link>https://johna.compoutpost.com/blog/963/how-to-add-ajax-suggestions-to-yaireo-s-tagify-tag-input-component/</link>
<description>&lt;div class=&quot;alert alert-info&quot;&gt;This post applies to Tagify v1.3.1 which has since been replaced by v2.x.&lt;br&gt;&lt;br&gt;I've since written my own very simple tag entry &lt;a href=&quot;/blog/1096/&quot;&gt;plugin&lt;/a&gt;.&lt;/div&gt;When looking for a tag input component, like the one used on Stack Overflow, I came across &lt;a href=&quot;https://github.com/yairEO/tagify&quot; target=&quot;_blank&quot;&gt;Tagify&lt;/a&gt;.&lt;br&gt;&lt;br&gt;This is a great implementation of a tag component, being very lightweight and using in-built browser behaviour where possible, for example rather than creating an auto suggest function like most other similar components, the creator uses a HTML 5 datalist element.&lt;br&gt;&lt;br&gt;I chose to use this component for one of the websites I am developing, but I need to make some changes to make it work how I want it to.&lt;br&gt;&lt;br&gt;The first change is to get suggestions via AJAX rather than a fixed whitelist. A fixed whitelist is fine for most applications, but in my case there could be thousands of possible tags and I was concerned about putting the entire list on the client.&lt;br&gt;&lt;br&gt;Some other changes that I need to make will be covered in a future post. For example, as my website uses Bootstrap I want the tag input component to look like a native Bootstrap control.&lt;br&gt;&lt;br&gt;Note that if you use this component with Bootstrap that you will have to hide the original INPUT or TEXTAREA control yourself as Tagify attempts to hide it using CSS (&lt;strong&gt;tags input, tags textarea { display: none; }&lt;/strong&gt;) which is overriden by Bootstrap's CSS.&lt;br&gt;&lt;br&gt;For graceful degradation, hiding this with CSS is not ideal, so using JavaScript is better anyway. I simply use the following after initialising Tagify on the control:&lt;br&gt;&lt;br&gt;&lt;pre&gt;$(&quot;#myinput&quot;).tagify(); //initialise Tagify&lt;br&gt;$(&quot;#myinput&quot;).hide(); // Hide the original control&lt;/pre&gt;&lt;br&gt;My modified version of Tagify is at the end of this article. I've added comments to indicate additions and modifications to the original code.&lt;br&gt;&lt;br&gt;There's an additional setting, &lt;strong&gt;suggestionsUrl&lt;/strong&gt; for the URL of the service that will provide suggestions via HTTP GET. The search term will be appended to this, so you may need to include part of your querystring in the URL, eg. &lt;strong&gt;&quot;https://domain.com/services/tagify.php?tag=&quot;&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;Tagify's &lt;strong&gt;autocomplete&lt;/strong&gt; setting also needs to be set to true, and you should not include values for both &lt;strong&gt;whitelist&lt;/strong&gt; and &lt;strong&gt;suggestionsUrl&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;Note that the HTML 5 datalist auto suggest works by finding the search term anywhere inside the list of suggestions, so you should do the same in your service that provides suggestions. ie. that means you shouldn't provide suggestions based on the first characters only.&lt;br&gt;&lt;br&gt;Your service should return a JSON result similar to this example (based on input of &lt;strong&gt;&quot;ab&quot;&lt;/strong&gt;):&lt;br&gt;&lt;br&gt;&lt;pre&gt;[&quot;ABAP&quot;,&quot;ABC&quot;,&quot;ABC ALGOL&quot;,&quot;ABSET&quot;,&quot;ABSYS&quot;]&lt;/pre&gt;&lt;br&gt;Okay, here's the code...&lt;br&gt;&lt;br&gt;&lt;pre&gt;/**&lt;br&gt; * Tagify (v 1.3.1)- tags input component&lt;br&gt; * By Yair Even-Or (2016)&lt;br&gt; * Don't sell this code. (c)&lt;br&gt; * https://github.com/yairEO/tagify&lt;br&gt;&lt;br&gt; &lt;span class=&quot;text-danger&quot;&gt;* Modifications for AJAX suggestions by John Avis 2018-05-25&lt;/span&gt;&lt;br&gt; */&lt;br&gt;;(function($){&lt;br&gt;    // just a jQuery wrapper for the vanilla version of this component&lt;br&gt;    $.fn.tagify = function(settings){&lt;br&gt;        return this.each(function() {&lt;br&gt;            var $input = $(this),&lt;br&gt;                tagify;&lt;br&gt;&lt;br&gt;            if( $input.data(&quot;tagify&quot;) ) // don't continue if already &quot;tagified&quot;&lt;br&gt;                return this;&lt;br&gt;&lt;br&gt;            tagify = new Tagify($input[0], settings);&lt;br&gt;            tagify.isJQueryPlugin = true;&lt;br&gt;            $input.data(&quot;tagify&quot;, tagify);&lt;br&gt;        });&lt;br&gt;    }&lt;br&gt;&lt;br&gt;function Tagify( input, settings ){&lt;br&gt;    // protection&lt;br&gt;    if( !input ){&lt;br&gt;        console.warn('Tagify: ', 'invalid input element ', input)&lt;br&gt;        return this;&lt;br&gt;    }&lt;br&gt;&lt;br&gt;    this.settings = this.extend({}, settings, this.DEFAULTS);&lt;br&gt;    this.settings.readonly = input.hasAttribute('readonly'); // if &quot;readonly&quot; do not include an &quot;input&quot; element inside the Tags component&lt;br&gt;&lt;br&gt;    this.legacyFixes();&lt;br&gt;&lt;br&gt;    if( input.pattern )&lt;br&gt;        try {&lt;br&gt;            this.settings.pattern = new RegExp(input.pattern);&lt;br&gt;        } catch(e){}&lt;br&gt;&lt;br&gt;    if( settings &amp;&amp; settings.delimiters ){&lt;br&gt;        try {&lt;br&gt;            this.settings.delimiters = new RegExp(&quot;[&quot; + settings.delimiters + &quot;]&quot;);&lt;br&gt;        } catch(e){}&lt;br&gt;    }&lt;br&gt;&lt;br&gt;    this.id = Math.random().toString(36).substr(2,9), // almost-random ID (because, fuck it)&lt;br&gt;    this.value = []; // An array holding all the (currently used) tags&lt;br&gt;    this.DOM = {}; // Store all relevant DOM elements in an Object&lt;br&gt;    this.extend(this, new this.EventDispatcher());&lt;br&gt;    this.build(input);&lt;br&gt;    this.events();&lt;br&gt;    &lt;span class=&quot;text-danger&quot;&gt;this.lastSuggestion = &quot;&quot;; //***Added***&lt;/span&gt;&lt;br&gt;}&lt;br&gt;&lt;br&gt;Tagify.prototype = {&lt;br&gt;    DEFAULTS : {&lt;br&gt;        delimiters          : &quot;,&quot;,        // [regex] split tags by any of these delimiters&lt;br&gt;        pattern             : &quot;&quot;,         // pattern to validate input by&lt;br&gt;        callbacks           : {},         // exposed callbacks object to be triggered on certain events&lt;br&gt;        duplicates          : false,      // flag - allow tuplicate tags&lt;br&gt;        enforceWhitelist    : false,      // flag - should ONLY use tags allowed in whitelist&lt;br&gt;        autocomplete        : true,       // flag - show native suggeestions list as you type&lt;br&gt;        whitelist           : [],         // is this list has any items, then only allow tags from this list&lt;br&gt;        blacklist           : [],         // a list of non-allowed tags&lt;br&gt;        maxTags             : Infinity,   // maximum number of tags&lt;br&gt;        suggestionsMinChars : 2,          // minimum characters to input to see sugegstions list&lt;br&gt;        maxSuggestions      : 10&lt;span class=&quot;text-danger&quot;&gt;, // ***Modified***&lt;br&gt;        suggestionsUrl      : &quot;&quot; // ***Added***&lt;/span&gt;&lt;br&gt;    },&lt;br&gt;&lt;br&gt;    /**&lt;br&gt;     * Fixes which require backword support&lt;br&gt;     */&lt;br&gt;    legacyFixes : function(){&lt;br&gt;        var _s = this.settings;&lt;br&gt;&lt;br&gt;        // For backwards compatibility with older versions, which use 'enforeWhitelist' instead of 'enforceWhitelist'.&lt;br&gt;        if( _s.hasOwnProperty(&quot;enforeWhitelist&quot;) &amp;&amp; !_s.hasOwnProperty(&quot;enforceWhitelist&quot;) ){&lt;br&gt;            _s[&quot;enforceWhitelist&quot;] = _s[&quot;enforeWhitelist&quot;];&lt;br&gt;            delete _s[&quot;enforeWhitelist&quot;];&lt;br&gt;            console.warn(&quot;Please update your Tagify settings. The 'enforeWhitelist' property is deprecated and you should be using 'enforceWhitelist'.&quot;);&lt;br&gt;        }&lt;br&gt;    },&lt;br&gt;&lt;br&gt;    /**&lt;br&gt;     * builds the HTML of this component&lt;br&gt;     * @param  {Object} input [DOM element which would be &quot;transformed&quot; into &quot;Tags&quot;]&lt;br&gt;     */&lt;br&gt;    build : function( input ){&lt;br&gt;        var that = this,&lt;br&gt;            value = input.value,&lt;br&gt;            inputHTML = '&amp;lt;div&amp;gt;&amp;lt;input list=&quot;tagifySuggestions'+ this.id +'&quot; class=&quot;placeholder&quot;/&amp;gt;&amp;lt;span&amp;gt;'+ input.placeholder +'&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;';&lt;br&gt;        this.DOM.originalInput = input;&lt;br&gt;        this.DOM.scope = document.createElement('tags');&lt;br&gt;        input.className &amp;&amp; (this.DOM.scope.className = input.className); // copy any class names from the original input element to the Tags element&lt;br&gt;        this.DOM.scope.innerHTML = inputHTML;&lt;br&gt;        this.DOM.input = this.DOM.scope.querySelector('input');&lt;br&gt;&lt;br&gt;        if( this.settings.readonly )&lt;br&gt;            this.DOM.scope.classList.add('readonly')&lt;br&gt;&lt;br&gt;        input.parentNode.insertBefore(this.DOM.scope, input);&lt;br&gt;        this.DOM.scope.appendChild(input);&lt;br&gt;&lt;br&gt;        // if &quot;autocomplete&quot; flag on toggeled &amp; &quot;whitelist&quot; has items, build suggestions list&lt;br&gt;        &lt;span class=&quot;text-danger&quot;&gt;if( this.settings.autocomplete &amp;&amp; (this.settings.whitelist.length || this.settings.suggestionsUrl != '') ){ // ***Modified***&lt;/span&gt;&lt;br&gt;            if( &quot;suggestions&quot; in this )&lt;br&gt;                this.suggestions.init();&lt;br&gt;            else&lt;br&gt;                this.DOM.datalist = this.buildDataList();&lt;br&gt;        }&lt;br&gt;&lt;br&gt;        // if the original input already had any value (tags)&lt;br&gt;        if( value )&lt;br&gt;            this.addTags(value).forEach(function(tag){&lt;br&gt;                tag &amp;&amp; tag.classList.add('tagify--noAnim');&lt;br&gt;            });&lt;br&gt;    },&lt;br&gt;&lt;br&gt;    /**&lt;br&gt;     * Reverts back any changes made by this component&lt;br&gt;     */&lt;br&gt;    destroy : function(){&lt;br&gt;        this.DOM.scope.parentNode.appendChild(this.DOM.originalInput);&lt;br&gt;        this.DOM.scope.parentNode.removeChild(this.DOM.scope);&lt;br&gt;    },&lt;br&gt;&lt;br&gt;    /**&lt;br&gt;     * Merge two objects into a new one&lt;br&gt;     */&lt;br&gt;    extend : function(o, o1, o2){&lt;br&gt;        if( !(o instanceof Object) ) o = {};&lt;br&gt;&lt;br&gt;        if( o2 ){&lt;br&gt;            copy(o, o2)&lt;br&gt;            copy(o, o1)&lt;br&gt;        }&lt;br&gt;        else&lt;br&gt;            copy(o, o1)&lt;br&gt;&lt;br&gt;        function copy(a,b){&lt;br&gt;            // copy o2 to o&lt;br&gt;            for( var key in b )&lt;br&gt;                if( b.hasOwnProperty(key) )&lt;br&gt;                    a[key] = b[key];&lt;br&gt;        }&lt;br&gt;&lt;br&gt;        return o;&lt;br&gt;    },&lt;br&gt;&lt;br&gt;    /**&lt;br&gt;     * A constructor for exposing events to the outside&lt;br&gt;     */&lt;br&gt;    EventDispatcher : function(){&lt;br&gt;        // Create a DOM EventTarget object&lt;br&gt;        var target = document.createTextNode('');&lt;br&gt;&lt;br&gt;        // Pass EventTarget interface calls to DOM EventTarget object&lt;br&gt;        this.off = target.removeEventListener.bind(target);&lt;br&gt;        this.on = target.addEventListener.bind(target);&lt;br&gt;        this.trigger = function(eventName, data){&lt;br&gt;            var e;&lt;br&gt;            if( !eventName ) return;&lt;br&gt;&lt;br&gt;            if( this.isJQueryPlugin )&lt;br&gt;                $(this.DOM.originalInput).triggerHandler(eventName, [data])&lt;br&gt;            else{&lt;br&gt;                try {&lt;br&gt;                    e = new CustomEvent(eventName, {&quot;detail&quot;:data});&lt;br&gt;                }&lt;br&gt;                catch(err){&lt;br&gt;                    e = document.createEvent(&quot;Event&quot;);&lt;br&gt;                    e.initEvent(&quot;toggle&quot;, false, false);&lt;br&gt;                }&lt;br&gt;                target.dispatchEvent(e);&lt;br&gt;            }&lt;br&gt;        }&lt;br&gt;    },&lt;br&gt;&lt;br&gt;    /**&lt;br&gt;     * DOM events listeners binding&lt;br&gt;     */&lt;br&gt;    events : function(){&lt;br&gt;        var that = this,&lt;br&gt;            events = {&lt;br&gt;                 //  event name / event callback / element to be listening to&lt;br&gt;                paste   : ['onPaste'      , 'input'],&lt;br&gt;                focus   : ['onFocusBlur'  , 'input'],&lt;br&gt;                blur    : ['onFocusBlur'  , 'input'],&lt;br&gt;                input   : ['onInput'      , 'input'],&lt;br&gt;                keydown : ['onKeydown'    , 'input'],&lt;br&gt;                click   : ['onClickScope' , 'scope']&lt;br&gt;            },&lt;br&gt;            customList = ['add', 'remove', 'duplicate', 'maxTagsExceed', 'blacklisted', 'notWhitelisted'];&lt;br&gt;&lt;br&gt;        for( var e in events )&lt;br&gt;            this.DOM[events[e][1]].addEventListener(e, this.callbacks[events[e][0]].bind(this));&lt;br&gt;&lt;br&gt;        customList.forEach(function(name){&lt;br&gt;            that.on(name, that.settings.callbacks[name])&lt;br&gt;        })&lt;br&gt;&lt;br&gt;        if( this.isJQueryPlugin )&lt;br&gt;            $(this.DOM.originalInput).on('tagify.removeAllTags', this.removeAllTags.bind(this))&lt;br&gt;    },&lt;br&gt;&lt;br&gt;    /**&lt;br&gt;     * DOM events callbacks&lt;br&gt;     */&lt;br&gt;    callbacks : {&lt;br&gt;        onFocusBlur : function(e){&lt;br&gt;            var text =  e.target.value.trim();&lt;br&gt;&lt;br&gt;            if( e.type == &quot;focus&quot; )&lt;br&gt;                e.target.className = 'input';&lt;br&gt;            else if( e.type == &quot;blur&quot; &amp;&amp; text ){&lt;br&gt;                if( this.addTags(text).length )&lt;br&gt;                    e.target.value = '';&lt;br&gt;            }&lt;br&gt;            else{&lt;br&gt;                e.target.className = 'input placeholder';&lt;br&gt;                this.DOM.input.removeAttribute('style');&lt;br&gt;            }&lt;br&gt;        },&lt;br&gt;&lt;br&gt;        onKeydown : function(e){&lt;br&gt;            var s = e.target.value,&lt;br&gt;                lastTag,&lt;br&gt;                that = this;&lt;br&gt;&lt;br&gt;            if( e.key == &quot;Backspace&quot; &amp;&amp; (s == &quot;&quot; || s.charCodeAt(0) == 8203) ){&lt;br&gt;                lastTag = this.DOM.scope.querySelectorAll('tag:not(.tagify--hide)');&lt;br&gt;                lastTag = lastTag[lastTag.length - 1];&lt;br&gt;                this.removeTag( lastTag );&lt;br&gt;            }&lt;br&gt;            if( e.key == &quot;Escape&quot; ){&lt;br&gt;                e.target.value = '';&lt;br&gt;                e.target.blur();&lt;br&gt;            }&lt;br&gt;            if( e.key == &quot;Enter&quot; ){&lt;br&gt;                e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380&lt;br&gt;                if( this.addTags(s).length )&lt;br&gt;                    e.target.value = '';&lt;br&gt;                return false;&lt;br&gt;            }&lt;br&gt;            else{&lt;br&gt;                if( this.noneDatalistInput ) clearTimeout(this.noneDatalistInput);&lt;br&gt;                this.noneDatalistInput = setTimeout(function(){ that.noneDatalistInput = null }, 50);&lt;br&gt;            }&lt;br&gt;        },&lt;br&gt;&lt;br&gt;        onInput : function(e){&lt;br&gt;            var value = e.target.value,&lt;br&gt;                lastChar = value[value.length - 1],&lt;br&gt;                isDatalistInput = !this.noneDatalistInput &amp;&amp; value.length &amp;gt; 1,&lt;br&gt;                showSuggestions = value.length &amp;gt;= this.settings.suggestionsMinChars,&lt;br&gt;                datalistInDOM;&lt;br&gt;&lt;br&gt;            e.target.style.width = ((e.target.value.length + 1) * 7) + 'px';&lt;br&gt;&lt;br&gt;&lt;br&gt;            // if( value.indexOf(',') != -1 || isDatalistInput ){&lt;br&gt;            if( value.slice().search(this.settings.delimiters) != -1 || isDatalistInput ){&lt;br&gt;                if( this.addTags(value).length )&lt;br&gt;                    e.target.value = ''; // clear the input field's value&lt;br&gt;            }&lt;br&gt;            &lt;span class=&quot;text-danger&quot;&gt;else if (this.settings.autocomplete &amp;&amp; (this.settings.whitelist.length || this.settings.suggestionsUrl != '') ){ // ***Modified***&lt;/span&gt;&lt;br&gt;                datalistInDOM = this.DOM.input.parentNode.contains( this.DOM.datalist );&lt;br&gt;&lt;br&gt;                // if sugegstions should be hidden&lt;br&gt;                if( !showSuggestions &amp;&amp; datalistInDOM )&lt;br&gt;                    this.DOM.input.parentNode.removeChild(this.DOM.datalist)&lt;br&gt;                &lt;span class=&quot;text-danger&quot;&gt;else if( showSuggestions ){ // ***Modified***&lt;br&gt;&lt;br&gt;                    // ***Added/Modified***&lt;br&gt;                    var suggestion = value.substr(0, this.settings.suggestionsMinChars);&lt;br&gt;&lt;br&gt;                    if (!datalistInDOM || suggestion !== this.lastSuggestion) {&lt;br&gt;                        this.lastSuggestion = suggestion;&lt;br&gt;&lt;br&gt;                        var dom = this.DOM;&lt;br&gt;&lt;br&gt;                        if (this.settings.suggestionsUrl != '') {&lt;br&gt;                            if (datalistInDOM) dom.input.parentNode.removeChild(dom.datalist);&lt;br&gt;                            dom.input.parentNode.appendChild(dom.datalist);&lt;br&gt;                            $(dom.datalist).find('select option').remove();&lt;br&gt;&lt;br&gt;                            $.getJSON(this.settings.suggestionsUrl + this.lastSuggestion, function (data) {&lt;br&gt;                                $.each(data, function (index, item) {&lt;br&gt;                                    $(dom.datalist).find('select').append('&amp;lt;option&amp;gt;' + item + '&amp;lt;/option&amp;gt;');&lt;br&gt;                                });&lt;br&gt;                            });&lt;br&gt;                        }&lt;br&gt;                    }&lt;br&gt;                    // ***End of Added***&lt;/span&gt;&lt;br&gt;&lt;br&gt;                }&lt;br&gt;            }&lt;br&gt;        },&lt;br&gt;&lt;br&gt;        onPaste : function(e){&lt;br&gt;            var that = this;&lt;br&gt;            if( this.noneDatalistInput ) clearTimeout(this.noneDatalistInput);&lt;br&gt;            this.noneDatalistInput = setTimeout(function(){ that.noneDatalistInput = null }, 50);&lt;br&gt;        },&lt;br&gt;&lt;br&gt;        onClickScope : function(e){&lt;br&gt;            if( e.target.tagName == &quot;TAGS&quot; )&lt;br&gt;                this.DOM.input.focus();&lt;br&gt;            if( e.target.tagName == &quot;X&quot; ){&lt;br&gt;                this.removeTag( e.target.parentNode );&lt;br&gt;            }&lt;br&gt;        }&lt;br&gt;    },&lt;br&gt;&lt;br&gt;    /**&lt;br&gt;     * Build tags suggestions using HTML datalist&lt;br&gt;     * @return {[type]} [description]&lt;br&gt;     */&lt;br&gt;    buildDataList : function(){&lt;br&gt;        var OPTIONS = &quot;&quot;,&lt;br&gt;            i,&lt;br&gt;            datalist = document.createElement('datalist');&lt;br&gt;&lt;br&gt;        datalist.id = 'tagifySuggestions' + this.id;&lt;br&gt;        datalist.innerHTML = &quot;&amp;lt;label&amp;gt; &lt;br&gt;                                select from the list: &lt;br&gt;                                &amp;lt;select&amp;gt; &lt;br&gt;                                    &amp;lt;option value=''&amp;gt;&amp;lt;/option&amp;gt; &lt;br&gt;                                    [OPTIONS] &lt;br&gt;                                &amp;lt;/select&amp;gt; &lt;br&gt;                            &amp;lt;/label&amp;gt;&quot;;&lt;br&gt;&lt;br&gt;        &lt;span class=&quot;text-danger&quot;&gt;if (this.settings.whitelist.length) { // ***Added***&lt;/span&gt;&lt;br&gt;            for( i=this.settings.whitelist.length; i--; )&lt;br&gt;                OPTIONS += &quot;&amp;lt;option&amp;gt;&quot;+ this.settings.whitelist[i] +&quot;&amp;lt;/option&amp;gt;&quot;;&lt;br&gt;        &lt;span class=&quot;text-danger&quot;&gt;} // ***Added***&lt;/span&gt;&lt;br&gt;&lt;br&gt;        datalist.innerHTML = datalist.innerHTML.replace('[OPTIONS]', OPTIONS); // inject the options string in the right place&lt;br&gt;&lt;br&gt;      //  this.DOM.input.insertAdjacentHTML('afterend', datalist); // append the datalist HTML string in the Tags&lt;br&gt;&lt;br&gt;        return datalist;&lt;br&gt;    },&lt;br&gt;&lt;br&gt;    getNodeIndex : function( node ){&lt;br&gt;        var index = 0;&lt;br&gt;        while( (node = node.previousSibling) )&lt;br&gt;            if (node.nodeType != 3 || !/^s*$/.test(node.data))&lt;br&gt;                index++;&lt;br&gt;        return index;&lt;br&gt;    },&lt;br&gt;&lt;br&gt;    /**&lt;br&gt;     * Searches if any tag with a certain value already exis&lt;br&gt;     * @param  {String} s [text value to search for]&lt;br&gt;     * @return {boolean}  [found / not found]&lt;br&gt;     */&lt;br&gt;    isTagDuplicate : function(s){&lt;br&gt;        return this.value.some(function(item){ return s.toLowerCase() === item.value.toLowerCase() });&lt;br&gt;    },&lt;br&gt;&lt;br&gt;    /**&lt;br&gt;     * Mark a tag element by its value&lt;br&gt;     * @param  {String / Number} value  [text value to search for]&lt;br&gt;     * @param  {Object}          tagElm [a specific &quot;tag&quot; element to compare to the other tag elements siblings]&lt;br&gt;     * @return {boolean}                [found / not found]&lt;br&gt;     */&lt;br&gt;    markTagByValue : function(value, tagElm){&lt;br&gt;        var tagsElms, tagsElmsLen;&lt;br&gt;&lt;br&gt;        if( !tagElm ){&lt;br&gt;            tagsElms = this.DOM.scope.querySelectorAll('tag');&lt;br&gt;            for( tagsElmsLen = tagsElms.length; tagsElmsLen--; ){&lt;br&gt;                if( tagsElms[tagsElmsLen].textContent.toLowerCase().includes(value.toLowerCase()) )&lt;br&gt;                    tagElm = tagsElms[tagsElmsLen];&lt;br&gt;            }&lt;br&gt;        }&lt;br&gt;&lt;br&gt;        // check AGAIN if &quot;tagElm&quot; is defined&lt;br&gt;        if( tagElm ){&lt;br&gt;            tagElm.classList.add('tagify--mark');&lt;br&gt;            setTimeout(function(){ tagElm.classList.remove('tagify--mark') }, 2000);&lt;br&gt;            return true;&lt;br&gt;        }&lt;br&gt;&lt;br&gt;        else{&lt;br&gt;&lt;br&gt;        }&lt;br&gt;&lt;br&gt;        return false;&lt;br&gt;    },&lt;br&gt;&lt;br&gt;    /**&lt;br&gt;     * make sure the tag, or words in it, is not in the blacklist&lt;br&gt;     */&lt;br&gt;    isTagBlacklisted : function(v){&lt;br&gt;        v = v.split(' ');&lt;br&gt;        return this.settings.blacklist.filter(function(x){ return v.indexOf(x) != -1 }).length;&lt;br&gt;    },&lt;br&gt;&lt;br&gt;    /**&lt;br&gt;     * make sure the tag, or words in it, is not in the blacklist&lt;br&gt;     */&lt;br&gt;    isTagWhitelisted : function(v){&lt;br&gt;        return this.settings.whitelist.indexOf(v) != -1;&lt;br&gt;    },&lt;br&gt;&lt;br&gt;    /**&lt;br&gt;     * add a &quot;tag&quot; element to the &quot;tags&quot; component&lt;br&gt;     * @param  {String/Array} tagsItems [A string (single or multiple values with a delimiter), or an Array of Objects]&lt;br&gt;     * @return {Array} Array of DOM elements (tags)&lt;br&gt;     */&lt;br&gt;    addTags : function( tagsItems ){&lt;br&gt;        var that = this,&lt;br&gt;            tagElems = [];&lt;br&gt;&lt;br&gt;        this.DOM.input.removeAttribute('style');&lt;br&gt;&lt;br&gt;        /**&lt;br&gt;         * pre-proccess the tagsItems, which can be a complex tagsItems like an Array of Objects or a string comprised of multiple words&lt;br&gt;         * so each item should be iterated on and a tag created for.&lt;br&gt;         * @return {Array} [Array of Objects]&lt;br&gt;         */&lt;br&gt;        function normalizeTags(tagsItems){&lt;br&gt;            var whitelistWithProps = this.settings.whitelist[0] instanceof Object,&lt;br&gt;                isComplex = tagsItems instanceof Array &amp;&amp; &quot;value&quot; in tagsItems[0], // checks if the value is a &quot;complex&quot; which means an Array of Objects, each object is a tag&lt;br&gt;                result = tagsItems; // the returned result&lt;br&gt;&lt;br&gt;            // no need to continue if &quot;tagsItems&quot; is an Array of Objects&lt;br&gt;            if( isComplex )&lt;br&gt;                return result;&lt;br&gt;&lt;br&gt;            // search if the tag exists in the whitelist as an Object (has props), to be able to use its properties&lt;br&gt;            if( !isComplex &amp;&amp; typeof tagsItems == &quot;string&quot; &amp;&amp; whitelistWithProps ){&lt;br&gt;                var matchObj = this.settings.whitelist.filter(function(item){&lt;br&gt;                    return item.value.toLowerCase() == tagsItems.toLowerCase();&lt;br&gt;                })&lt;br&gt;&lt;br&gt;                if( matchObj[0] ){&lt;br&gt;                    isComplex = true;&lt;br&gt;                    result = matchObj; // set the Array (with the found Object) as the new value&lt;br&gt;                }&lt;br&gt;            }&lt;br&gt;&lt;br&gt;            // if the value is a &quot;simple&quot; String, ex: &quot;aaa, bbb, ccc&quot;&lt;br&gt;            if( !isComplex ){&lt;br&gt;                tagsItems = tagsItems.trim();&lt;br&gt;                if( !tagsItems ) return [];&lt;br&gt;&lt;br&gt;                // go over each tag and add it (if there were multiple ones)&lt;br&gt;                result = tagsItems.split(this.settings.delimiters).map(function(v){&lt;br&gt;                    return { value:v.trim() }&lt;br&gt;                });&lt;br&gt;            }&lt;br&gt;&lt;br&gt;            return result.filter(function(n){ return n }); // cleanup the array from &quot;undefined&quot;, &quot;false&quot; or empty items;&lt;br&gt;        }&lt;br&gt;&lt;br&gt;        /**&lt;br&gt;         * validate a tag object BEFORE the actual tag will be created &amp; appeneded&lt;br&gt;         * @param  {Object} tagData  [{&quot;value&quot;:&quot;text&quot;, &quot;class&quot;:whatever&quot;, ...}]&lt;br&gt;         * @return {Boolean/String}  [&quot;true&quot; if validation has passed, String or &quot;false&quot; for any type of error]&lt;br&gt;         */&lt;br&gt;        function validateTag( tagData ){&lt;br&gt;            var value = tagData.value.trim(),&lt;br&gt;                maxTagsExceed = this.value.length &amp;gt;= this.settings.maxTags,&lt;br&gt;                isDuplicate,&lt;br&gt;                eventName__error,&lt;br&gt;                tagAllowed;&lt;br&gt;&lt;br&gt;            // check for empty value&lt;br&gt;            if( !value )&lt;br&gt;                return &quot;empty&quot;;&lt;br&gt;&lt;br&gt;            // check if pattern should be used and if so, use it to test the value&lt;br&gt;            if( this.settings.pattern &amp;&amp; !(this.settings.pattern.test(value)) )&lt;br&gt;                return &quot;pattern&quot;;&lt;br&gt;&lt;br&gt;            // check if the tag already exists&lt;br&gt;            if( this.isTagDuplicate(value) ){&lt;br&gt;                this.trigger('duplicate', value);&lt;br&gt;&lt;br&gt;                if( !this.settings.duplicates ){&lt;br&gt;                    // this.markTagByValue(value, tagElm)&lt;br&gt;                    return &quot;duplicate&quot;;&lt;br&gt;                }&lt;br&gt;            }&lt;br&gt;&lt;br&gt;            // check if the tag is allowed by the rules set&lt;br&gt;            tagAllowed = !this.isTagBlacklisted(value) &amp;&amp; (!this.settings.enforceWhitelist || this.isTagWhitelisted(value)) &amp;&amp; !maxTagsExceed;&lt;br&gt;&lt;br&gt;            // Check against blacklist &amp; whitelist (if enforced)&lt;br&gt;            if( !tagAllowed ){&lt;br&gt;                tagData.class = tagData.class ? tagData.class + &quot; tagify--notAllowed&quot; : &quot;tagify--notAllowed&quot;;&lt;br&gt;&lt;br&gt;                // broadcast why the tag was not allowed&lt;br&gt;                if( maxTagsExceed )                                                        eventName__error = 'maxTagsExceed';&lt;br&gt;                else if( this.isTagBlacklisted(value) )                                    eventName__error = 'blacklisted';&lt;br&gt;                else if( this.settings.enforceWhitelist &amp;&amp; !this.isTagWhitelisted(value) ) eventName__error = 'notWhitelisted';&lt;br&gt;&lt;br&gt;                this.trigger(eventName__error, {value:value, index:this.value.length});&lt;br&gt;&lt;br&gt;                return &quot;notAllowed&quot;;&lt;br&gt;            }&lt;br&gt;&lt;br&gt;            return true;&lt;br&gt;        }&lt;br&gt;&lt;br&gt;        /**&lt;br&gt;         * appened (validated) tag to the component's DOM scope&lt;br&gt;         * @return {[type]} [description]&lt;br&gt;         */&lt;br&gt;        function appendTag(tagElm){&lt;br&gt;            this.DOM.scope.insertBefore(tagElm, this.DOM.input.parentNode);&lt;br&gt;        }&lt;br&gt;&lt;br&gt;        //////////////////////&lt;br&gt;        tagsItems = normalizeTags.call(this, tagsItems);&lt;br&gt;&lt;br&gt;        tagsItems.forEach(function(tagData){&lt;br&gt;            var isTagValidated = validateTag.call(that, tagData);&lt;br&gt;&lt;br&gt;            if( isTagValidated === true || isTagValidated == &quot;notAllowed&quot; ){&lt;br&gt;                // create the tag element&lt;br&gt;                var tagElm = that.createTagElem(tagData);&lt;br&gt;&lt;br&gt;                // add the tag to the component's DOM&lt;br&gt;                appendTag.call(that, tagElm);&lt;br&gt;&lt;br&gt;                // remove the tag &quot;slowly&quot;&lt;br&gt;                if( isTagValidated == &quot;notAllowed&quot; ){&lt;br&gt;                    setTimeout(function(){ that.removeTag(tagElm, true) }, 1000);&lt;br&gt;                }&lt;br&gt;&lt;br&gt;                else{&lt;br&gt;                    // update state&lt;br&gt;                    that.value.push(tagData);&lt;br&gt;                    that.update();&lt;br&gt;                    that.trigger('add', that.extend({}, tagData, {index:that.value.length, tag:tagElm}));&lt;br&gt;&lt;br&gt;                    tagElems.push(tagElm);&lt;br&gt;                }&lt;br&gt;            }&lt;br&gt;        })&lt;br&gt;&lt;br&gt;        return tagElems&lt;br&gt;    },&lt;br&gt;&lt;br&gt;    /**&lt;br&gt;     * creates a DOM tag element and injects it into the component (this.DOM.scope)&lt;br&gt;     * @param  Object}  tagData [text value &amp; properties for the created tag]&lt;br&gt;     * @return {Object} [DOM element]&lt;br&gt;     */&lt;br&gt;    createTagElem : function(tagData){&lt;br&gt;        var tagElm = document.createElement('tag');&lt;br&gt;&lt;br&gt;        // for a certain Tag element, add attributes.&lt;br&gt;        function addTagAttrs(tagElm, tagData){&lt;br&gt;            var i, keys = Object.keys(tagData);&lt;br&gt;            for( i=keys.length; i--; ){&lt;br&gt;                var propName = keys[i];&lt;br&gt;                if( !tagData.hasOwnProperty(propName) ) return;&lt;br&gt;                tagElm.setAttribute(propName, tagData[propName] );&lt;br&gt;            }&lt;br&gt;        }&lt;br&gt;&lt;br&gt;        // The space below is important - http://stackoverflow.com/a/19668740/104380&lt;br&gt;        tagElm.innerHTML = &quot;&amp;lt;x&amp;gt;&amp;lt;/x&amp;gt;&amp;lt;div&amp;gt;&amp;lt;span title='&quot;+ tagData.value +&quot;'&amp;gt;&quot;+ tagData.value +&quot; &amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;&quot;;&lt;br&gt;&lt;br&gt;        // add any attribuets, if exists&lt;br&gt;        addTagAttrs(tagElm, tagData);&lt;br&gt;&lt;br&gt;        return tagElm;&lt;br&gt;    },&lt;br&gt;&lt;br&gt;    /**&lt;br&gt;     * Removes a tag&lt;br&gt;     * @param  {Object}  tagElm    [DOM element]&lt;br&gt;     * @param  {Boolean} silent    [A flag, which when turned on, does not removes any value and does not update the original input value but simply removes the tag from tagify]&lt;br&gt;     */&lt;br&gt;    removeTag : function( tagElm, silent ){&lt;br&gt;        var tagData,&lt;br&gt;            tagIdx = this.getNodeIndex(tagElm);&lt;br&gt;&lt;br&gt;        if( !tagElm) return;&lt;br&gt;&lt;br&gt;        tagElm.style.width = parseFloat(window.getComputedStyle(tagElm).width) + 'px';&lt;br&gt;        document.body.clientTop; // force repaint for the width to take affect before the &quot;hide&quot; class below&lt;br&gt;        tagElm.classList.add('tagify--hide');&lt;br&gt;&lt;br&gt;        // manual timeout (hack, since transitionend cannot be used because of hover)&lt;br&gt;        setTimeout(function(){&lt;br&gt;            tagElm.parentNode.removeChild(tagElm);&lt;br&gt;        }, 400);&lt;br&gt;&lt;br&gt;        if( !silent ){&lt;br&gt;            tagData = this.value.splice(tagIdx, 1)[0]; // remove the tag from the data object&lt;br&gt;            this.update(); // update the original input with the current value&lt;br&gt;            this.trigger('remove', this.extend({}, tagData, {index:tagIdx, tag:tagElm}));&lt;br&gt;        }&lt;br&gt;    },&lt;br&gt;&lt;br&gt;    removeAllTags : function(){&lt;br&gt;        this.value = [];&lt;br&gt;        this.update();&lt;br&gt;        Array.prototype.slice.call(this.DOM.scope.querySelectorAll('tag')).forEach(function(elm){&lt;br&gt;            elm.parentNode.removeChild(elm);&lt;br&gt;        });&lt;br&gt;    },&lt;br&gt;&lt;br&gt;    /**&lt;br&gt;     * update the origianl (hidden) input field's value&lt;br&gt;     */&lt;br&gt;    update : function(){&lt;br&gt;        var tagsAsString = this.value.map(function(v){ return v.value }).join(',');&lt;br&gt;        this.DOM.originalInput.value = tagsAsString;&lt;br&gt;    }&lt;br&gt;}&lt;br&gt;&lt;br&gt;})(jQuery);&lt;/pre&gt;</description>
<comments>https://johna.compoutpost.com/blog/963/how-to-add-ajax-suggestions-to-yaireo-s-tagify-tag-input-component/#comments</comments>
<pubDate>2018-05-25T12:00:00+10:00</pubDate>
<category>Web Development</category>
<guid>https://johna.compoutpost.com/blog/963</guid>
</item>
<item>
<title>10 things a web developer should be careful of</title>
<link>https://johna.compoutpost.com/blog/955/10-things-a-web-developer-should-be-careful-of/</link>
<description>&lt;p&gt;I am a bit old-fashioned when it comes to web development. As much as I like the fancy new ways of doing things, I see so many websites where things go wrong and end up frustrating users.&lt;/p&gt;&lt;p&gt;Viewing websites on mobile phones is often the most frustrating, particularly responsive websites where the same website is designed to run on both a &quot;resource-limited&quot; mobile as well as a &quot;resource-rich&quot; desktop.&lt;/p&gt;&lt;p&gt;Here's my list of ten things web developers should be careful of.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;h2&gt;Browser detection&lt;/h2&gt;&lt;p&gt;I have a Windows phone (yes, I am the one) and there's one company that sends me a regular email with an offer that sometimes I want to activate so I click the link in the email on my phone and it takes me to a web page. The &quot;clever&quot; developers have decided that because they don't recognise my browser's user agent that they will just show me an error message that my browser is not compatible. The offer doesn't get activated and I can't view any other pages on that website either. They have decided that their precious website can only be used if they know what browser is in use, just so it can display a message saying an offer has been accepted.&lt;/p&gt;&lt;p&gt;In this case it would be much better to at least perform the server-side action of activating the offer, and attempt to display a success message.&lt;/p&gt;&lt;p&gt;Here's another case of browser detection gone too far. A front end developer creating a new website for a large organisation decided that he should display a (non-dismissable) message across the top of every page of the website if he detected that the user was using an old browser that wasn't fully compatible with whatever HTML/CSS features he had used. Something like this...&lt;/p&gt;&lt;p&gt;&lt;em&gt;&quot;Your browser is out of date. Please visit xxx to update your browser.&quot;&lt;/em&gt;&lt;/p&gt;&lt;p&gt;Nice. Unfortunately the several thousand staff in this organisation were all locked into using a specific browser that just happened to be old enough that they would all see this message and they were not actually able to update their browser due to organisational policy.&lt;/p&gt;&lt;p&gt;So please don't tell people what they probably already know. Chances are users know they are not operating on the latest browser, and are either unable to update it for some reason or don't want to update it.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;h2&gt;Make your website accessible&lt;/h2&gt;&lt;p&gt;Many web developers know just the basics of web accessibility and nothing more.&lt;/p&gt;&lt;p&gt;My advice to every web developer is to actually try a screen reader and other assistive technology yourself and see what it's like to navigate a web page.&lt;/p&gt;&lt;p&gt;You'll soon learn how frustrating it must be for people to use web pages even if the website is properly accessible. And how infuriating it must be if it isn't done well.&lt;/p&gt;&lt;p&gt;You'll learn the importance of properly structured navigation, content and why often a blank image &quot;alt&quot; tag is better than your 50 word description of some image that you just used to make the page look nice. And you'll find out all about the joys of HTML tables and accessibility.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;h2&gt;Be careful about what you do on page load&lt;/h2&gt;&lt;p&gt;Don't you hate it when you go to a web page, one that is a little slow to load fully, and you start reading down the page, then some clever web developer decides that the page has now fully loaded and you should be scrolled to a specific part of the page, or you start typing into a text box and suddenly the focus is given to a different control?&lt;/p&gt;&lt;p&gt;Be careful with what and when you do these types of things.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;h2&gt;AJAX is great, but...&lt;/h2&gt;&lt;p&gt;AJAX has some great advantages and can make pages operate quicker and more like &quot;desktop&quot; applications, but you have to get them right.&lt;/p&gt;&lt;p&gt;Here's some things that can go wrong:&lt;/p&gt;&lt;p&gt;History: many people rely on the back button. If you have some nice infinity load list and someone clicks through to one of the items on the list and then clicks back, what happens? Are they taken back to the exact same list they just saw? Or are they taken back to the top and then have to infinity load back to where they were? Many websites don't get this right (because it can be difficult).&lt;/p&gt;&lt;p&gt;Bookmarks: On a list page, maybe there's a bunch of filters and I've set them up with how I want to see the list and bookmarked it so I can always come back to that list. Imagine my surprise when they filters are all AJAX and the URL is not updated so I just bookmark the standard page instead!&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;h2&gt;Allow open in new window/tab&lt;/h2&gt;&lt;p&gt;Some websites accidentally block opening of hyperlinks in new windows/tabs. Middle-clicking the hyperlink just opens in the same window/tab, and/or the actually hyperlink is not a &quot;proper&quot; html hyperlink, just some other element with some JavaScript to make it work so right-clicking doesn't bring up a hyperlink-appropriate context menu.&lt;/p&gt;&lt;p&gt;Some people (like me) often like to open a heap of items from a list page at the same time without having to keep going back to the list.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;h2&gt;Speed&lt;/h2&gt;&lt;p&gt;Google know how frustrating slow websites can be, so they rank slow websites below similar faster websites. They even back AMP (Accelerated Mobile Pages) in an attempt to speed the web up (&lt;a href=&quot;/blog/804/i-hope-accelerated-mobile-pages-amp-fail/&quot;&gt;I don't like AMP very much though&lt;/a&gt;).&lt;/p&gt;&lt;p&gt;I think that one of the biggest threats to performance is too much JavaScript. These days many web developers seem dependent on front-end frameworks that have a heap of JavaScript plus dependencies on other frameworks and libraries that also have a heap of JavaScript.&lt;/p&gt;&lt;p&gt;I'm guilty of this too with my preference towards Bootstrap and jQuery (and their dependencies). Part of this is just laziness. See &lt;a href=&quot;http://youmightnotneedjquery.com/&quot; target=&quot;_blank&quot;&gt;youmightnotneedjquery.com&lt;/a&gt; for how simple it is to do most things without jQuery.&lt;/p&gt;&lt;p&gt;But advertising scripts are probably the worst offenders. We've &quot;advanced&quot; from simple banner and text advertising to complex dynamic advertisements that scroll through pages of items you previously looked at on some other website.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;h2&gt;Times have changed, people are used to seeing more on the page&lt;/h2&gt;&lt;p&gt;In the early days of the web, pages with lists - such as products on an ecommerce website - used to be short, like maybe ten products per page.&lt;/p&gt;&lt;p&gt;These days people are used to seeing a lot more items per list before they have to load the next page, or even be able to see everything perhaps with infinity scroll.&lt;/p&gt;&lt;p&gt;I still see many recent websites that have ridiculously small lists that requires a lot of pagination to get through a long list, and an equally frustrating trip through browser history to get back to that other page you want to have a second look at.&lt;/p&gt;&lt;p&gt;As per tip 4, using AJAX for lists pagination or infinity scrolling needs to be done very carefully otherwise it can be equally frustrating. The middle ground is perhaps non-AJAX pagination but with a generous amount of items per page.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;h2&gt;Don't hide too much on mobile&lt;/h2&gt;&lt;p&gt;Some websites have separate mobile versions or are responsive and hide a lot of features that are available on a desktop.&lt;/p&gt;&lt;p&gt;This can be very frustrating if you know about that feature from using on a desktop previously but can't find it when viewing on a mobile.&lt;/p&gt;&lt;p&gt;I'm all for hiding superfluous content on the mobile, sometimes even things like breadcrumbs that can take up valuable real estate at the top of the page, but generally every feature should be accessible and available on all device versions.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;h2&gt;Test properly&lt;/h2&gt;&lt;p&gt;Often web developers have high-end mobile phones and desktops and do a lot of testing in their own high-speed development environment.&lt;/p&gt;&lt;p&gt;You won't really know how well your website works in the &quot;real world&quot; unless you test in a similar environment to what your users will be doing. That means possibly a low-end phone, wi-fi or 3G, away from your local server network, and on a typically loaded server.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;h2&gt;Don't override normal behaviour&lt;/h2&gt;&lt;p&gt;I recently visited a website that had an autocomplete type search.&lt;/p&gt;&lt;p&gt;The problem was that as I was typing, it was performing an AJAX query, and while it was waiting for the server response any keystrokes were ignored.&lt;/p&gt;&lt;p&gt;I typed &quot;the quick brown fox jumps over the lazy dog&quot; and it showed as &quot;the quck brown fox umps ve the lay dog&quot;.  And I'm not even a fast typist.&lt;/p&gt;&lt;p&gt;Best to let the browser do it's job here, and just listen in to what's being typed without intercepting it.&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;</description>
<comments>https://johna.compoutpost.com/blog/955/10-things-a-web-developer-should-be-careful-of/#comments</comments>
<pubDate>2018-04-23T12:00:00+10:00</pubDate>
<category>Web Development</category>
<guid>https://johna.compoutpost.com/blog/955</guid>
</item>
<item>
<title>Why Microsoft's ASP/ASP.NET may be the safe choice for development</title>
<link>https://johna.compoutpost.com/blog/925/why-microsoft-s-asp-asp-net-may-be-the-best-choice-for-development-in-some-cases/</link>
<description>&lt;img alt=&quot;&quot; src=&quot;/blog/uploads/img925_safe.jpg&quot; class=&quot;img-fluid&quot; /&gt;&lt;br&gt;&lt;br&gt;Your situation may be different but in my case, over close to 20 years I have created many websites and applications for myself and clients, many that are still in use today.&lt;br&gt;&lt;br&gt;Most, if not all, have under gone some changes over the years. In some cases this might just be a cosmetic refresh and the server-side code remains the same or features may have been added or changed over the years.&lt;br&gt;&lt;br&gt;Of course, due to the nature of the web, some get total rewrites after a short time, but many clients don&#8217;t need or want to spend money on major rewrites.&lt;br&gt;&lt;br&gt;Fortunately, the majority of the websites and applications I have developed were done so using the Microsoft technologies Classic ASP and ASP.NET.&lt;br&gt;&lt;br&gt;If, like me, you created something in the late 1990s using Classic ASP and didn&#8217;t do anything stupid like not protect against SQL injection or unsafe user uploads, then your code could still run today on a modern web server running a current version of operating system and IIS without you losing sleep that there were security problems waiting to bite you.&lt;br&gt;&lt;br&gt;And if, like me, you wrote something in ASP.NET 1.x or 2.x in the early 2000s, again, as long as it was running on a modern server, you could feel safe and without fear of potential security problems.&lt;br&gt;&lt;br&gt;Now, let&#8217;s compare this to PHP (and let me say I am no expert on PHP but do have some experience with it).&lt;br&gt;&lt;br&gt;If you wrote something in an earlier version of PHP, because there are breaking changes between major versions, you either still need to be running on that same version of PHP, or it&#8217;s time to review and migrate to the current version of PHP.&lt;br&gt;&lt;br&gt;An online search reveals that some earlier versions of PHP have security flaws and the advice is to update to safe versions.&lt;br&gt;&lt;br&gt;&lt;strong&gt;What about these popular open source frameworks that are so popular now?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;These days it&#8217;s a very common for developers to use new frameworks, usually open source, to develop their websites and applications. There seems to be a new one out every month or so.&lt;br&gt;&lt;br&gt;Is this a good idea to use these? Will an application written using one of these frameworks still be okay in a few years&#8217; time?&lt;br&gt;&lt;br&gt;Here&#8217;s a hypothetical look at what the future might hold for such an application&#8230;&lt;br&gt;&lt;br&gt;1. Our application is developed in version 2 of a fictional JavaScript framework. Version 1 was well received but had some security and performance issues which have been resolved by a major rewrite. The fictional JavaScript framework leverages off a couple of other JavaScript frameworks.&lt;br&gt;&lt;br&gt;2. Our application works great and gets a few enhancements over the next year or two.&lt;br&gt;&lt;br&gt;3. Version 3 of the fictional JavaScript framework gets introduced and has some great new features and fixes some security and performance issues with version 2. There are some breaking changes from the previous version so our application code will need to be reviewed and migrated.&lt;br&gt;&lt;br&gt;4. Our application is quite large, possibly with hundreds of thousands of lines of code, and reviewing and migration is a major project that we would rather not undertake.&lt;br&gt;&lt;br&gt;5. Interest and support for version 2 of the fictional framework has dropped off and as version 3 was not well received another fictional framework has become the popular choice. It is strongly recommended not to use version 2 anymore because of security flaws in it and/or one of its dependencies. Because of this, and your development team are excited about using the new fictional framework, a rewrite is recommend.&lt;br&gt;&lt;br&gt;Did I mention I still have Classic ASP and early .NET Web Forms applications out there in the wild and I don&#8217;t lose any sleep over them at all?</description>
<comments>https://johna.compoutpost.com/blog/925/why-microsoft-s-asp-asp-net-may-be-the-best-choice-for-development-in-some-cases/#comments</comments>
<pubDate>2018-02-15T12:00:00+10:00</pubDate>
<category>Web Development</category>
<image>https://johna.compoutpost.com/blog/uploads/img925_safe.jpg</image>
<guid>https://johna.compoutpost.com/blog/925</guid>
</item>
<item>
<title>The command line interface is back</title>
<link>https://johna.compoutpost.com/blog/829/the-command-line-interface-is-back/</link>
<description>&lt;img alt=&quot;Command prompt&quot; src=&quot;/blog/uploads/img829_command-prompt.jpg&quot; class=&quot;img-responsive&quot; /&gt;&lt;br&gt;&lt;br&gt;Back in the 1980s and early 1990s I considered myself a bit of an expert at the MS-DOS &lt;a href=&quot;https://en.wikipedia.org/wiki/Command-line_interface&quot; target=&quot;_blank&quot;&gt;command line interface&lt;/a&gt; and knew all the commands and all of the options for each command. &lt;br&gt;&lt;br&gt;Then Windows 3.x came along and things started moving away from the command line until Windows 95, when the command line was no longer needed by the average user. Everything could be done with a &lt;a href=&quot;https://en.wikipedia.org/wiki/Graphical_user_interface&quot; target=&quot;_blank&quot;&gt;GUI&lt;/a&gt;.&lt;br&gt;&lt;br&gt;You could then administer most functions of the computer, even set up the operating system without the need to type in any commands.&lt;br&gt;&lt;br&gt;I was a late adopter of Windows, but became comfortable in a GUI world and was happy to be free of the command line interface.&lt;br&gt;&lt;br&gt;But slowly things have been regressing back to the command line interface.&lt;br&gt;&lt;br&gt;I blame Linux. Even today, some familiarity with the command line is essential for using a Linux operating system.&lt;br&gt;&lt;br&gt;But unfortunately, the return of the command line interface is not just limited to using a Linux OS.&lt;br&gt;&lt;br&gt;As a web developer, I am now inundated with various command line interfaces, some of which offer no GUI alternative. I appreciate that most people in this profession are most likely to be advanced computer users and not afraid of command line interfaces, but for how many purposes is a CLI really better than a GUI?&lt;br&gt;&lt;br&gt;Source control systems, tools for installing add-ons and dependencies, and even configuring some applications now often requires typing of commands and editing of text-based configuration files.&lt;br&gt;&lt;br&gt;Can we go back to the future now, please?</description>
<comments>https://johna.compoutpost.com/blog/829/the-command-line-interface-is-back/#comments</comments>
<pubDate>2017-04-28T12:00:00+10:00</pubDate>
<category>Web Development</category>
<image>https://johna.compoutpost.com/blog/uploads/img829_command-prompt.jpg</image>
<guid>https://johna.compoutpost.com/blog/829</guid>
</item>
<item>
<title>Is ASP.NET better than PHP?</title>
<link>https://johna.compoutpost.com/blog/815/is-asp-net-better-than-php/</link>
<description>Firstly, I know PHP is a programming language and ASP.NET is a framework so we're not comparing apples with apples here, but still people make the choice whether they are going to learn how to develop in PHP or ASP.NET so this comparison is valid!&lt;br&gt;&lt;br&gt;This posts stems from my own curiosity. It's a question not a statement. I've been developing in ASP.NET for many years and I like it. I've dabbled a bit in PHP and there are some things I like and some I don't like, but maybe I haven't immersed myself in it enough to know what's great about it...&lt;br&gt;&lt;br&gt;So here's what I think is great about ASP.NET. Can you tell me how things are from the PHP side?&lt;br&gt;&lt;br&gt;&lt;h2&gt;Visual Studio&lt;/h2&gt;(Microsoft's IDE for ASP.NET development)&lt;br&gt;&lt;br&gt;Visual Studio is awesome. Why? For a start it autocompletes commands, objects, methods, properties and almost everything else as I type and it has an extra level of intelligence that almost can't be explained. If I do something like &lt;kbd&gt;object1.Field = object2.Field&lt;/kbd&gt; and both objects have the same property name then it gives that property as the first choice after I type the part &lt;kbd&gt;object2.&lt;/kbd&gt;. And as I start typing something it gives the last similar name as the first choice, which is useful when you are writing code and repetitively typing the same object names.&lt;br&gt;&lt;br&gt;VS will tell me when there's an error - a red wavy underline tells you something's seriously wrong - and it will tell me if something doesn't make sense - a green wavy underline might show me something like code that's unreachable, or accessing a value where all possible code paths don't set a value.&lt;br&gt;&lt;br&gt;It also has great navigability. What do I mean? Moving around your code is easy. Recently worked on  areas are highlighted. You can jump to a line number easily. &lt;kbd&gt;Ctrl+c&lt;/kbd&gt; with no selection copies the current line, and pasting a whole line with &lt;kbd&gt;CTRL+v&lt;/kbd&gt; doesn't require you to pin-point an exact location - VS will put it above the current line if your cursor is in the middle of another line. Correct indenting happens automatically when it should.&lt;br&gt;&lt;br&gt;I can do all sorts of things with code windows and toolbars... Dock them, split the screen, move them to other screens!&lt;br&gt;&lt;br&gt;This is only just scratching the surface of VS's features, and all of this power comes at a price of as little as $zero (there's nothing wrong with the free version either).&lt;br&gt;&lt;br&gt;&lt;h2&gt;Debugging&lt;/h2&gt;Also a feature of Visual Studio and it's integrated web server is debugging. I can watch as each line executes and check values. I can set breakpoints and watches.&lt;br&gt;&lt;br&gt;&lt;h2&gt;Compilation&lt;/h2&gt;You don't have to compile all ASP.NET projects, but it is very useful to do so.&lt;br&gt;&lt;br&gt;You can't compile a project if there are errors in the coding. Most markup errors will not show up in compilation but coding errors will result in a failed build.&lt;br&gt;&lt;br&gt;Better to know at compile time than after copying changes to a test or live website, right?&lt;br&gt;&lt;br&gt;&lt;h2&gt;Choice of development style&lt;/h2&gt;Just like PHP and Classic ASP you can code top-to-bottom style using ASP.NET &quot;Web Pages&quot;. Although aimed at beginners and PHP converts, you have full access to the power of C# and the .NET framework. You don't need to compile, you can just copy files to your web folder and they will be compiled automatically as needed.&lt;br&gt;&lt;br&gt;Prefer the more modern and trendy MVC? You can do that too.&lt;br&gt;&lt;br&gt;Or what about every anti-Microsoft-er's &lt;em&gt;favourite&lt;/em&gt;: &quot;Web Forms&quot;. Hated almost everywhere for its abstraction from everything (shouldn't we all be writing assembly code then?), inefficient Viewstate (you know you can turn it off right?) and lack of compliance with modern standards (Microsoft could have updated it...).&lt;br&gt;&lt;br&gt;My opinion on &quot;Web Forms&quot;? It's a work of genius! But like most systems it's simple to make simple applications, but difficult to make difficult applications. You can do a lot wrong with it if you don't have experience with it... Just like any other form of development.&lt;br&gt;&lt;br&gt;If you have to make something like a real complicated web form with lots of AJAX for hiding and showing things then &quot;Web Forms&quot; and UpdatePanels are hard to beat. But I'm sure you won't believe me. I've got one such horrendously complex form that has 2,400 lines of markup and another 7,850 lines of code-behind - I don't know how I could have done that in any other system (I'm bracing myself for the &quot;why would you&quot; comments now).&lt;br&gt;&lt;br&gt;&lt;h2&gt;Strongly typed&lt;/h2&gt;I know why some people don't like strongly typed objects but it makes things so much easier. I don't know how people can cope without it? All this checking for this and that, and then can it be converted to this, okay then convert it. It drives me crazy when I have to switch back to Classic ASP and do all that.&lt;br&gt;&lt;br&gt;&lt;h2&gt;Linq to SQL&lt;/h2&gt;Recently I was wanting to go back to writing plain SQL queries so I could write the absolute most efficient data access for a performance critical project (only because it was going to be run on cheap shared hosting). I didn't last through more than a couple of pages before I realised that doing things this way seems ridiculous in 2016.&lt;br&gt;&lt;br&gt;Linq to SQL (or Linq to &lt;em&gt;x&lt;/em&gt;), despite its faults, is the only way I want to query databases now. &lt;br&gt;&lt;br&gt;If you don't know about it, it allows you to write database queries within your code that are similar to SQL. You can pretty much do most things in L2S as you can in SQL, and the syntax is similar (but admittedly some things need a bit of extra work). You can also keep adding to your queries before they actually go to the database server (eg. if you need to build a complex query based on conditions).&lt;br&gt;&lt;br&gt;The great thing about L2S is that your database structure is known so your queries are strongly typed. You can't mistype a table or column name.&lt;br&gt;&lt;br&gt;I know there's ORMs and the like out there which will easily generate simple queries for you without having to write your own SQL queries but, I don't know about you, but most of my SQL queries are not just simple &quot;SELECT WHERE&quot; type queries. I usually have all sorts of joins and conditions and counts and other stuff going on.&lt;br&gt;&lt;br&gt;Now if only Microsoft would make Linq to SQL even more like SQL (close to identical would be nice) then I would be really happy.&lt;br&gt;&lt;br&gt;&lt;h2&gt;It's in the framework&lt;/h2&gt;Almost everything you need is already baked into the framework. Image manipulation, email, database access, even authentication and authorisation if you like. It &lt;em&gt;can&lt;/em&gt; be rare to &lt;em&gt;have&lt;/em&gt; to go outside the framework for most tasks. I know choice is a great thing but sometimes not having to make a choice is better. &lt;br&gt;&lt;br&gt;So how do these things compare to PHP? What do you like in PHP that makes development better than ASP.NET?&lt;br&gt;&lt;br&gt;&lt;h2&gt;2023 Update&lt;/h2&gt;My reaction to reading this post seven years later?&lt;br&gt;&lt;br&gt;I moved from ASP.NET to PHP in my wok and now have a different perspective.&lt;br&gt;&lt;br&gt;Firstly, PHP Storm in an excellent IDE for PHP and comparable to Visual Studio except there is no free edition. You can do everything that I mentioned VS can do.&lt;br&gt;&lt;br&gt;.NET has evolved, and not everything is good. All code must now be compiled. The framework no longer has everything you need. At least MVC is no longer being pushed as the answer to everything. Linq is still awesome for writing queries.&lt;br&gt;&lt;br&gt;PHP has also evolved. It appears to be slowly becoming strongly-typed... Well, kind of. I don't know why they don't just go all-out and introduce a configuration option and give people the option of what they want.</description>
<comments>https://johna.compoutpost.com/blog/815/is-asp-net-better-than-php/#comments</comments>
<pubDate>2016-09-21T12:00:00+10:00</pubDate>
<category>Web Development</category>
<guid>https://johna.compoutpost.com/blog/815</guid>
</item>
<item>
<title>I hope Accelerated Mobile Pages (AMP) fail</title>
<link>https://johna.compoutpost.com/blog/804/i-hope-accelerated-mobile-pages-amp-fail/</link>
<description>It's not often I wish such bad fortune on a new innovation but I am wishing &lt;a href=&quot;https://www.ampproject.org/&quot; target=&quot;_blank&quot;&gt;Accelerated Mobile Pages (AMP)&lt;/a&gt; dies a quick death.&lt;br&gt;&lt;br&gt;It's not that I don't agree that websites should be MUCH more efficient and load quicker.&lt;br&gt;&lt;br&gt;It's just that I don't agree that this is the way to make it happen.&lt;br&gt;&lt;br&gt;If you don't already know about AMP, in 25 words or less it's a way of creating web pages that are small and faster loading. AMP pages cannot contain certain HTML (like forms), only include images using a special AMP tag, only allow approved scripts, don't allow external style sheets, and the total page size must be less than 50kb.&lt;br&gt;&lt;br&gt;The most important thing you need to know about AMP is that Google are backing it, and &quot;supporting&quot; it by making it a ranking factor.&lt;br&gt;&lt;br&gt;Not that long in the past many of us developers were creating websites that were optimised for a specific resolution, usually a maximum width of 1024 pixels so targeted at desktop screens.&lt;br&gt;&lt;br&gt;Then mobile browsing became popular and many of us started creating mobile only websites. These were usually cut down versions of the desktop site and were faster less bandwidth hungry (and usually uglier) but meant that we now had to maintain two websites.&lt;br&gt;&lt;br&gt;Then along came &quot;responsive design&quot; and we were able to create one website for all devices.&lt;br&gt;&lt;br&gt;Now Google wants us to create AMP versions of all our web pages.&lt;br&gt;&lt;br&gt;I'm not sure the world wants cut down versions of web pages. As a frequent mobile browser (someone who browses, not something like Internet Explorer) myself, there are times when I appreciate a simple, easy to read alternate mobile only version of a web page, but there are also many times when I can't find some relevant feature on a mobile page that I am used to seeing on the equivalent desktop page. Often enough, I find myself looking for a &quot;view full site&quot; hyperlink.&lt;br&gt;&lt;br&gt;Given the high rate of technical advances in mobile technology, and the ongoing reduction in cost of mobile data, I hope that AMP might not be relevant.&lt;h2&gt;One-way Web?&lt;/h2&gt;As I said earlier in this post, AMP doesn't allow HTML forms. This might be okay for some types of websites, but many websites (possibly most) have a real purpose for forms such as posting a comment on a blog or news website, subscribing to a newsletter, or buying something on an ecommerce website. How do you accommodate this? Move these things to separate pages, and then you would probably want to style them the same as the AMP pages which means you are now maintaining a desktop version of the site, AMP version of the site, and these AMP spin-off pages.&lt;br&gt;&lt;br&gt;Is your website supported by advertising? Well, only certain advertising platforms' scripts are approved, so hopefully you use one of them and not one of the unsupported platforms. Google AdWords is supported, of course.&lt;br&gt;&lt;br&gt;Disallowing unapproved scripts mean that you won't be able to add any AJAX either. AMP pretty much makes the web one-way. How 1990s!&lt;h2&gt;So, if I am against AMP, what could Google do instead?&lt;/h2&gt;I think they already had some good things going like suggestions for optimisations and improvements in Google Search Console. Telling people what's wrong with their websites and how to fix them is a good step.&lt;br&gt;&lt;br&gt;Punishing us for producing slow websites, as much as I won't like being a victim, is also probably the right thing to do.&lt;br&gt;&lt;br&gt;But how about if they got involved in some of these open source systems that are used by so many websites and improving them? I am talking about open source applications such as CMS and eCommerce, many of which are horribly inefficient. And popular open source frameworks like Bootstrap and jQuery.</description>
<comments>https://johna.compoutpost.com/blog/804/i-hope-accelerated-mobile-pages-amp-fail/#comments</comments>
<pubDate>2016-05-23T12:00:00+10:00</pubDate>
<category>Computers & Internet</category>
<category>Web Development</category>
<guid>https://johna.compoutpost.com/blog/804</guid>
</item>
<item>
<title>Web developers: Don't feel too bad if your websites have a few bugs</title>
<link>https://johna.compoutpost.com/blog/800/web-developers-don-t-feel-too-bad-if-your-websites-have-a-few-bugs/</link>
<description>I know there are bugs in most (all?) of the websites I have developed, and I feel bad when I find a bug and wonder how long it's been there.&lt;br&gt;&lt;br&gt;But no matter how many bugs there are in your code, there's probably a worse website... Yahoo!&lt;br&gt;&lt;br&gt;It amazes me how a large company like Yahoo - with all their resources and no doubt a large team of programmers - can have so many serious bugs in their website.&lt;br&gt;&lt;br&gt;I recently wanted to investigate Yahoo's Gemini advertising service, as I am getting a bit sick of Google AdWords and it's ridiculous number of resources that need to load just to show one advertisement.&lt;br&gt;&lt;br&gt;First issue... I have an old Yahoo account, and I can log in to this account, but it seems to be an old account that doesn't allow me to make any changes to update my account, just an error message&quot;Yahoo Profile (profile.yahoo.com) is no longer active&quot; and a link to learn more about the error that doesn't really help at all.&lt;br&gt;&lt;br&gt;Okay, no problem, I'll create a new account. Second issue... The Yahoo CAPTCHA is showing a mixture of upper and lower case so I enter the characters exactly as I see them. It takes me a few failed attempts until I try all lower case, even though they show as upper case, and I finally succeed.&lt;br&gt;&lt;br&gt;Let's check my account settings and set my preferences. From the Yahoo home page I click my name in the top right corner which brings up a drop down menu of choices. Third issue... As soon as my mouse leaves my name the drop down disappears. It seems impossible to select anything from this menu! Why have a click-to-activate drop down menu that disappears on hover?&lt;br&gt;&lt;br&gt;&lt;img alt=&quot;Yahoo account impossible&quot; class=&quot;img-responsive&quot; src=&quot;/blog/uploads/img800_yahoo-account.jpg&quot;&gt;&lt;br&gt;&lt;br&gt;I move on to Yahoo's Gemini website and attempt to sign up. At this point I am using my Windows Phone 8.1 and trying to select my country but - fourth issue - Yahoo are using their own SELECT replacement drop down menus which do not let me scroll through the choices, so although I want to select Australia, I am stuck down at the end of the alphabet because United States is the default value.&lt;br&gt;&lt;br&gt;I switch to the desktop and I can now select my country but when I attempt to select my time zone I see - fifth issue - that there are duplicate time zones in the list with no obvious difference. Oh well, choose anyone I guess.&lt;br&gt;&lt;br&gt;&lt;img alt=&quot;Duplicate time zones&quot; class=&quot;img-responsive&quot; src=&quot;/blog/uploads/img800_yahoo-dropdowns.jpg&quot;&gt;&lt;br&gt;&lt;br&gt;Ready to sign up now? Nope, sixth issue, I see a message saying that only US based companies are supported at this time. Why not show this message before I fill out the form? Why allow me to select from several hundred countries if only one is supported?&lt;br&gt;&lt;br&gt;&lt;img alt=&quot;Only US&quot; class=&quot;img-responsive&quot; src=&quot;/blog/uploads/img800_yahoo-only-usa.jpg&quot;&gt;&lt;br&gt;&lt;br&gt;Time to give up on Yahoo!</description>
<comments>https://johna.compoutpost.com/blog/800/web-developers-don-t-feel-too-bad-if-your-websites-have-a-few-bugs/#comments</comments>
<pubDate>2016-03-21T12:00:00+10:00</pubDate>
<category>Web Development</category>
<image>https://johna.compoutpost.com/blog/uploads/img800_yahoo-account.jpg</image>
<guid>https://johna.compoutpost.com/blog/800</guid>
</item>
<item>
<title>Another website project for motoring magazine collectors</title>
<link>https://johna.compoutpost.com/blog/797/another-website-project-for-motoring-magazine-collectors/</link>
<description>&lt;img alt=&quot;magazine-collector-website.jpg&quot; src=&quot;/blog/uploads/img797_magazine-collector-website.jpg&quot; class=&quot;img-responsive&quot; /&gt;&lt;br&gt;&lt;br&gt;I'm working on a new website for one of my hobbies &amp;ndash; car magazine collecting.&lt;br&gt;&lt;br&gt;I wanted a website where I could create an index of the contents of each magazine so I would be able to search for articles about particular makes and models.&lt;br&gt;&lt;br&gt;I also wanted to be able to track which issues I have, which ones I want, and which ones I don't want and would like to sell.&lt;br&gt;&lt;br&gt;A wiki-like website where anyone could add and update magazine contents sounded ideal.&lt;br&gt;&lt;br&gt;I love &lt;a href=&quot;http://stackoverflow.com/&quot; target=&quot;_blank&quot;&gt;stackoverflow.com&lt;/a&gt; and have taken some inspiration from that site for how to handle members, permissions and points.&lt;br&gt;&lt;h2&gt;So, what to develop in?&lt;/h2&gt;&lt;br&gt;Well I work in ASP.NET Web Forms in my day job but I do get a bit annoyed by some of its features &amp;ndash; mainly the bloat and issues with ViewState. It would have been the quickest option as I am so familiar with it.&lt;br&gt;&lt;br&gt;I thought about ASP.NET MVC and I really should take a leap into this as it seems it is now the professional choice for ASP.NET development. However, I don't like it.&lt;br&gt;&lt;br&gt;I decided upon ASP.NET Web Pages. One of the main reasons is that I can do development changes and fixes easily without the need for compilation and redeployment. Just a change to a cshtml file is all that's needed.&lt;br&gt;&lt;br&gt;Although Web Pages is regarded as a framework for beginners, it does expose all the benefits of ASP.NET and C#, so it is just as capable as any of the other choices.&lt;br&gt;&lt;br&gt;On the client side I have chosen to start with &lt;a href=&quot;http://v4-alpha.getbootstrap.com/&quot; target=&quot;_blank&quot;&gt;Bootstrap 4&lt;/a&gt;. Although still in Alpha stage I thought it would be a good idea to get acquainted with it, and enjoy the benefits of smaller file size, improvements and new features.&lt;br&gt;&lt;h2&gt;And what about data access?&lt;/h2&gt;&lt;br&gt;Because this (not profitable) website would be run on a low-cost shared hosting plan, performance would be fairly critical. My original plan for data access was to use the SqlClient namespace and SqlDataReaders, etc for everything. After all, that's similar to how we did things in the &quot;old days&quot; - raw SQL and straight from database to web page.&lt;br&gt;&lt;br&gt;However, it didn't take long for me to tire of doing it this way. I prefer writing raw SQL but years of Linq to SQL and Entity Framework made me yearn for strongly typed queries and results.&lt;br&gt;&lt;br&gt;So I started switching over the Linq to SQL. A lot less bloat than EF but it really speeds development. Some of my more complex queries are a bit of a pain in Linq but that's the price you pay. Still, I hope someone answers my &lt;a href=&quot;/blog/789/web-and-database-development-and-servers-should-become-one-system/&quot;&gt;call&lt;/a&gt; for a better system that integrates application and database, and allows us to write raw SQL in our application better than Linq does.&lt;br&gt;&lt;br&gt;&lt;a href=&quot;http://www.magazinecollector.net/&quot; target=&quot;_blank&quot;&gt;magazinecollector.net&lt;/a&gt; is online but still a work in progress.</description>
<comments>https://johna.compoutpost.com/blog/797/another-website-project-for-motoring-magazine-collectors/#comments</comments>
<pubDate>2016-02-03T12:00:00+10:00</pubDate>
<category>Web Development</category>
<image>https://johna.compoutpost.com/blog/uploads/img797_magazine-collector-website.jpg</image>
<guid>https://johna.compoutpost.com/blog/797</guid>
</item>
</channel>
</rss>
