contenteditablejscripthta

Simple javascript functions to scroll next/previous H1 to top of screen - in an HTA application run with MSHTA.exe


I am a hobby programmer, and have made an HTA file, with some JavaScript and a single contentEditable <DIV>. Using minimal JavaScript I can make keyboard shortcuts to format, save, and exit.

I am using mshta32.exe to run the file, in a fully updated Windows 10 system. My empty HTA is only 12k and for this I get word wrapping, formatting, linked images, print (including to PDF) and live spellchecking! It s not a word processor, but for distraction free composition I am loving the experience.

My next goal is to use these files for presentations; the minimum functionality I need is a "next slide" and "previous slide" shortcut. It seems reasonable to use <H1> tags as slide titles and therefore my shortcuts only need to scroll so the next, or previous, <H1> is at the top of the screen (yes HTA does save tags in upper case).

On the internet/ChatGPT there are solutions using document.querySelector but this does not seem to be supported within HTA/MSHTA (I have tried a meta tag with content=ie=edge and http-equiv=X-UA-Compatible). Not keen on JQuery as this is a minimalist project and needs to work offline, so I tried translating a suggestion in jQuery:

making a skip button that scrolls to the next heading

$('html, body').scrollTop($(this).nextInDOM($('h1, h2, h3, h4, h5, h6')).position().top); 

event.preventDefault();

To JavaScript:

document.querySelector('html, body').scrollTop(document.querySelector(this).nextInDOM(document.querySelector('h1, h2, h3, h4, h5, h6')).position().top); 
event.preventDefault();

This did not work! Any guidance welcome.

Kind Regards Gavin Holt

Whole HTA

<!DOCTYPE html>
<HTML>
<HEAD unselectable="on">
<TITLE unselectable="on"> ContentEditor - O:\MyProfile\editor\templates\default.hta</TITLE><?XML:NAMESPACE PREFIX = "HTA" />
<HTA:Application id=document.currentScript VERSION="2" SYSMENU="yes" Singleinstance="no" ShowInTaskBar="yes" scroll="yes" NAVIGABLE="yes" MinimizeButton="yes" MaximizeButton="yes" Icon="O:\MyProfile\cmd\IcoFX\ContentEditor.ico" ContextMenu="No" Border="No" APPLICATIONNAME="MSI-BUILD"></HTA:Application>
<BASE target=_blank unselectable="on">
<META name=viewport content="width=device-width, initial-scale=1" unselectable="on">
<META content=ie=edge unselectable="on">
<META http-equiv=X-UA-Compatible unselectable="on">
<STYLE unselectable="on">
@media Unknown    
{
.sidenav {
    PADDING-TOP: 15px
}
    }
HTML {
    COLOR: #657b83; MARGIN: 1em; BACKGROUND-COLOR: #fdf6e3
}
BODY {
    FONT-SIZE: 24px; FONT-FAMILY: Helvetica, arial, sans-serif; LETTER-SPACING: 0px; BACKGROUND-COLOR: #fdf6e3
}
.sidenav {
    FONT-SIZE: 22px; OVERFLOW: hidden; HEIGHT: 100%; WIDTH: 0px; OVERFLOW-X: hidden; POSITION: fixed; PADDING-TOP: 4px; LEFT: 0px; Z-INDEX: 1; TOP: 0px; BACKGROUND-COLOR: #111; transition: 0.5s
}
.sidenav A {
    TEXT-DECORATION: none; COLOR: #818181; PADDING-BOTTOM: 8px; PADDING-TOP: 8px; PADDING-LEFT: 32px; DISPLAY: block; PADDING-RIGHT: 8px; transition: 0.3s
}
.sidenav P {
    COLOR: #818181; PADDING-BOTTOM: 4px; PADDING-TOP: 8px; PADDING-LEFT: 4px; PADDING-RIGHT: 4px
}
TD {
    COLOR: #818181; PADDING-BOTTOM: 4px; PADDING-TOP: 8px; PADDING-LEFT: 4px; PADDING-RIGHT: 4px
}
TH {
    COLOR: #818181; PADDING-BOTTOM: 4px; PADDING-TOP: 8px; PADDING-LEFT: 4px; PADDING-RIGHT: 4px
}
B {
    COLOR: #818181; PADDING-BOTTOM: 4px; PADDING-TOP: 8px; PADDING-LEFT: 4px; PADDING-RIGHT: 4px
}
U {
    COLOR: #818181; PADDING-BOTTOM: 4px; PADDING-TOP: 8px; PADDING-LEFT: 4px; PADDING-RIGHT: 4px
}
.sidenav A:hover {
    COLOR: #f1f1f1
}
.sidenav TABLE {
    BORDER-LEFT-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; LINE-HEIGHT: 1; BORDER-TOP-WIDTH: 0px
}
TR {
    BORDER-LEFT-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; LINE-HEIGHT: 1; BORDER-TOP-WIDTH: 0px
}
TD {
    BORDER-LEFT-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; LINE-HEIGHT: 1; BORDER-TOP-WIDTH: 0px
}
TH {
    BORDER-LEFT-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; LINE-HEIGHT: 1; BORDER-TOP-WIDTH: 0px
}
#editor {
    PADDING-BOTTOM: 16px; PADDING-TOP: 16px; PADDING-LEFT: 16px; PADDING-RIGHT: 16px; transition: margin-left .5s
}
H1 {
    FONT-SIZE: 24px; TEXT-ALIGN: left; LINE-HEIGHT: 1.2
}
H2 {
    FONT-SIZE: 24px; TEXT-ALIGN: left; LINE-HEIGHT: 1.2
}
H3 {
    FONT-SIZE: 24px; TEXT-ALIGN: left; LINE-HEIGHT: 1.2
}
P {
    FONT-SIZE: 24px; TEXT-ALIGN: left; LINE-HEIGHT: 1.2
}
A {
    FONT-SIZE: 24px; TEXT-ALIGN: left; LINE-HEIGHT: 1.2
}
CODE {
    FONT-SIZE: 24px; TEXT-ALIGN: left; LINE-HEIGHT: 1.2
}
H1 {
    FONT-SIZE: 26px; FONT-VARIANT: small-caps
}
TABLE {
    WIDTH: 95%; BORDER-COLLAPSE: collapse
}
TABLE {
    BORDER-TOP-STYLE: none; BORDER-LEFT-STYLE: none; BORDER-BOTTOM-STYLE: none; BORDER-RIGHT-STYLE: none
}
TD {
    BORDER-TOP-STYLE: none; BORDER-LEFT-STYLE: none; BORDER-BOTTOM-STYLE: none; BORDER-RIGHT-STYLE: none
}
TH {
    BORDER-TOP-STYLE: none; BORDER-LEFT-STYLE: none; BORDER-BOTTOM-STYLE: none; BORDER-RIGHT-STYLE: none
}
TR {
    VERTICAL-ALIGN: top; TEXT-ALIGN: left; LINE-HEIGHT: 1.2
}
TD {
    VERTICAL-ALIGN: top; TEXT-ALIGN: left; LINE-HEIGHT: 1.2
}
TD {
    
}
TH {
    
}
TABLE {
    PAGE-BREAK-INSIDE: auto
}
TR {
    PAGE-BREAK-INSIDE: avoid; PAGE-BREAK-AFTER: auto
}
THEAD {
    DISPLAY: table-header-group
}
TFOOT {
    DISPLAY: table-footer-group
}
</STYLE>

<SCRIPT language=JScript type=text/jscript unselectable="on">
<!--
function preventDefault(e){
    if (e.preventDefault) {
        e.preventDefault();
    } else {
        e.returnValue = false;
    }
}
function toggleNav() {
    if (document.getElementById("mySidenav").style.width=="260px") {
    closeNav();
    } else {
    openNav();
    }
}
function openNav() {
    document.getElementById("mySidenav").style.width = "260px";
    document.getElementById("editor").style.marginLeft = "260px";
    document.getElementById("mySidenav").style.overflow = "scroll";
}
function closeNav() {
    document.getElementById("mySidenav").style.width = "0";
    document.getElementById("editor").style.marginLeft= "0";
    document.getElementById("mySidenav").style.overflow = "hidden";
}
function writeFile(){
    // Deal with funny \ at work!
    var filename = window.location.pathname

    var fso, fileHandle;
    fso = new ActiveXObject("Scripting.FileSystemObject");
    fileHandle = fso.CreateTextFile(filename.replace(/\//,""), true);

    fileHandle.write("<!DOCTYPE html>");
    fileHandle.write("<HTML>");
    fileHandle.write(document.documentElement.innerHTML);
    fileHandle.write("</HTML>");
    fileHandle.close();
}
function getSelected() {
    if (window.getSelection) {
        return window.getSelection();
    } else if (document.getSelection) {
        return document.getSelection();
    } else {
        var selection = document.selection && document.selection.createRange();
        if (selection.text) {
            return selection.text;
        }
        return false;
    }
}
function insertText(text) {
    if (document.selection){
        var range = document.selection.createRange();
        range.pasteHTML(text);
    }
}
function followlink(){
    alert('followlink called');
}
function popupmenu(event) {
    alert('popupmenu called');
    // Need to know the event.target - but don't want jquery!
    // use window.screenLeft window.screenTop
}
function Shortcuts(e){
    if (!e) var e = window.event;

    var key = e.keyCode
    if (e.ctrlKey)  { key = "ctrl"+key;     }
    if (e.altKey)   { key = "alt"+key;      }
    if (e.shiftKey) { key = "shift"+key;    }

    // alert(key)

    // ESC
    if ( key == 27) {
        writeFile();
        window.close();
    }

    // TAB alone
    if ( key == 9) {
        preventDefault(e);
        document.execCommand("indent", true, null);
        return false;
    }

    // Shift+TAB
    if (key == "shift9") {
        preventDefault(e);
        document.execCommand("outdent", true, null);
        return false;
    }

    // Ctrl+>
    if ( key == "ctrl190") {
        document.execCommand("indent", true);
    }

    // Ctrl+<
    if ( key == "ctrl188") {
        document.execCommand("outdent", true);
    }

    // Ctrl+Up TODO: Fails with this key
    if ( key == "ctrl38") {
        document.execCommand("superscript", true);
    }

    // Ctrl+Down TODO: Fails with this key
    if ( key == "ctrl40") {
        document.execCommand("subscript", true);
    }

    // Ctrl++ - Built in zoom+
    // Ctrl+- - Built in zoom-

    // Ctrl+/ Try to wrap any selection
    if ( key == "ctrl191") {
        var sText = getSelected();
        var code =    "<code>" + sText +  "</code>"
        insertText(code);
    }

    // Ctrl+\ Clear formatting
    if ( key == "ctrl220") {
        document.execCommand("removeFormat", true);
    }

    // Ctrl+A - Built in select all
    // Ctrl+B - Built in bold
    // Ctrl+C - Built in copy

    // Ctrl+D
    if ( key == "ctrl68") {
        document.execCommand("strikethrough", true);
    }

    // Ctrl+E - Editor
    if ( key == "ctrl69") {
        WshShell = new ActiveXObject("WScript.Shell");
        WshShell.Run("O:/MyProfile/editor/micro.bat " + window.location.pathname,1,true)
    }

    // Ctrl+F - Built in Find
    // Ctrl+G - Google
    if ( key == "ctrl71") {
        WshShell = new ActiveXObject("WScript.Shell");
        WshShell.run("http://www.google.com");
    }

    // Ctrl+H - Hyperlink
    if ( key == "ctrl72") {
        var sText = getSelected();
        var linkURL = prompt('Enter a URL:', 'http://');
        // This is to match the automated format when URL is pasted/typed
        var link =  '<A href="' + linkURL + '" target="_blank">' + sText + '</A>';
        insertText(link);
    }

    // Ctrl+I - Built in italic
    // Ctrl+J - Jump to next H1 - for slide presentations
    if ( key == "ctrl74") {

    }

    // Ctrl+K - Built in hyperlink 

    // Ctrl+L - Ordered list
    if ( key == "ctrl76") {
        document.execCommand("insertUnorderedList", true);
    }

    // Ctrl+M 
    if ( key == "ctrl77") {
        // Undo default action of CRLF
        return false;
    }

    // Ctrl+N - Numbered List
    if ( key == "ctrl78") {
        document.execCommand("insertOrderedList", true);
    }

    // Ctrl+O - Empty
    // Ctrl+P - Built in Print
    // Ctrl+Q - Empty

    // Ctrl+R - Revert
    if ( key == "ctrl82") {
        window.location.reload(false);
    }

    // Ctrl+S - Save
    if ( key == "ctrl83") {
        writeFile();
    }

    // Ctrl+T - Insert Table  - TODO: need some css or in line styles
    if ( key == "ctrl84") {
        document.execCommand("indent", true, null);
        var table =    "<table>"
        table = table +"<tr>"
        table = table +"  <th>Company</th>"
        table = table +"  <th>Contact</th>"
        table = table +"  <th>Country</th>"
        table = table +"</tr>"
        table = table +"<tr>"
        table = table +"  <td>Alfreds</td>"
        table = table +"  <td>Maria</td>"
        table = table +"  <td>Germany</td>"
        table = table +"</tr>"
        table = table +"<tr>"
        table = table +"  <td>Centro</td>"
        table = table +"  <td>Francisco</td>"
        table = table +"  <td>Mexico</td>"
        table = table +"</tr>"
        table = table +"</table>"
        insertText(table);
    }

    // Ctrl+U - Built in Underline
    // Ctrl+V - Built in Paste

    // Ctrl+W - Write and Close
    if ( key == "ctrl87") {
        writeFile();
        window.close();
    }

    // Ctrl+X - Built in Cut
    // Ctrl+Y - Built in Redo 
    // Ctrl+Z - Built in Undo

    // F1
    if ( key == 112) {
        toggleNav();
    }

    // Ctrl+0
    if ( key == "ctrl48") {
        document.execCommand("formatBlock", false, "<P>");
    }

    // Ctrl+1
    if ( key == "ctrl49") {
        document.execCommand("formatBlock", false, "<H1>");
    }

    // Ctrl+2
    if ( key == "ctrl50") {
        document.execCommand("formatBlock", false, "<H2>");
    }

    // Ctrl+3
    if ( key == "ctrl51") {
        document.execCommand("formatBlock", false, "<H3>");
    }

}

// Try to stop focus outside my div
for (i=0; i<document.all.length; i++){
    //ensure that all document elements except the content editable DIV are unselectable
    document.all(i).unselectable = "on";
}

// Change title
var filename = window.location.pathname
document.title = " ContentEditor - " + filename.replace(/\//,"")

// Focus on the editable section
window.location.hash = '#editor';

// Settings
document.execCommand("LiveResize", null, true);

//-->
</SCRIPT>
</HEAD>
<BODY tabIndex=-1>
<DIV tabIndex=-1 id=mySidenav class=sidenav style="OVERFLOW: scroll; WIDTH: 260px" unselectable="on"><U><B>Help</B></U> 
<TABLE>
<TBODY>
<TR>
<TD>ESC </TD>
<TD>Save + exit</TD></TR>
<TR>
<TD>TAB </TD>
<TD>Indent</TD></TR>
<TR>
<TD>Shift+TAB </TD>
<TD>Outdent</TD></TR>
<TR>
<TD>Ctrl+A </TD>
<TD>Select All</TD></TR>
<TR>
<TD>Ctrl+B </TD>
<TD>Bold</TD></TR>
<TR>
<TD>Ctrl+C </TD>
<TD>Copy</TD></TR>
<TR>
<TD>Ctrl+D </TD>
<TD>Strikeout</TD></TR>
<TR>
<TD>Ctrl+E </TD>
<TD>Edit page</TD></TR>
<TR>
<TD>Ctrl+F </TD>
<TD>Find</TD></TR>
<TR>
<TD>Ctrl+I </TD>
<TD>Italic</TD></TR>
<TR>
<TD>Ctrl+K </TD>
<TD>Hyperlink</TD></TR>
<TR>
<TD>Ctrl+L </TD>
<TD>Bullet list</TD></TR>
<TR>
<TD>Ctrl+N </TD>
<TD>Num list</TD></TR>
<TR>
<TD>Ctrl+P </TD>
<TD>Print</TD></TR>
<TR>
<TD>Ctrl+R </TD>
<TD>Revert</TD></TR>
<TR>
<TD>Ctrl+S </TD>
<TD>Save</TD></TR>
<TR>
<TD>Ctrl+T </TD>
<TD>Insert table</TD></TR>
<TR>
<TD>Ctrl+U </TD>
<TD>Underline</TD></TR>
<TR>
<TD>Ctrl+V </TD>
<TD>Paste</TD></TR>
<TR>
<TD>Ctrl+W </TD>
<TD>Save + exit</TD></TR>
<TR>
<TD>Ctrl+X </TD>
<TD>Cut</TD></TR>
<TR>
<TD>Ctrl+Y </TD>
<TD>Redo</TD></TR>
<TR>
<TD>Ctrl+Z </TD>
<TD>Undo</TD></TR>
<TR>
<TD>Ctrl+/ </TD>
<TD>Code</TD></TR>
<TR>
<TD>Ctrl+&lt; </TD>
<TD>Superscript</TD></TR>
<TR>
<TD>Ctrl+&gt; </TD>
<TD>Subscript</TD></TR>
<TR>
<TD>Ctrl+\\ </TD>
<TD>Unformat</TD></TR>
<TR>
<TD>Ctrl+Del </TD>
<TD>Del EOW</TD></TR>
<TR>
<TD>Ctrl+Bksp </TD>
<TD>Del BOW</TD></TR>
<TR>
<TD>Ctrl+n </TD>
<TD>Heading 1-3</TD></TR>
<TR>
<TD>F1 </TD>
<TD>Toggle Help</TD></TR></TBODY></TABLE>
<P></P></DIV>
<DIV spellcheck=true tabIndex=-1 onkeyup=Shortcuts() id=editor contentEditable=true onkeydown="return (event.keyCode!=9);" style="MARGIN-LEFT: 260px" oncontextmenu=popupmenu()></DIV>
<SCRIPT type=text/javascript>
<!--

//-->
</SCRIPT>
</BODY></HTML>

Hi,

Many thanks for all the advice so far, in summary:

My writeFile() function will always add <?XML:NAMESPACE PREFIX = "HTA" />, which puts me in IE7 mode.

function writeFile(){
    // Deal with funny \ at work!
    var filename = window.location.pathname

    var fso, fileHandle;
    fso = new ActiveXObject("Scripting.FileSystemObject");
    fileHandle = fso.CreateTextFile(filename.replace(/\//,""), true);

    fileHandle.write("<!DOCTYPE html>");
    fileHandle.write("<HTML>");
    fileHandle.write(document.documentElement.innerHTML);
    fileHandle.write("</HTML>");
    fileHandle.close();
}

I need the <hta:application> section to for its control over the window and the icon.

<hta:application
    id=document.currentScript
    applicationname="MSI-BUILD"
    border="No"
    contextmenu="No"
    icon="O:\MyProfile\cmd\ContentEditor.ico"
    maximizebutton="yes"
    minimizebutton="yes"
    navigable="yes"
    scroll="yes"
    showintaskbar="yes"
    singleinstance="no"
    sysmenu="yes"
    version="2"
>

Using a function to see documentMode and ScriptEngine, I get IE7 with JScript 11.0.16384.

        ver = 'IE' + document.documentMode + ' , ';
        ver += ScriptEngine() + ' ';
        ver += ScriptEngineMajorVersion() + '.';
        ver += ScriptEngineMinorVersion() + '.';
        ver += ScriptEngineBuildVersion();
        alert(ver);

So, I think I am stuck with IE7 era scripting.

Kind Regards Gavin Holt


Solution

  • Your advice on how to ask ChatGPT has been very helpful, this function does exactly what I need:

    function scrollToNextH1() {
        var h1Elements = document.getElementsByTagName('h1');
        var currentH1Index = -1;
    
        for (var i = 0; i < h1Elements.length; i++) {
            if (h1Elements[i].getBoundingClientRect().top > 0) {
                currentH1Index = i;
                break;
            }
        }
    
        if (currentH1Index !== -1 && currentH1Index < h1Elements.length - 1) {
            var nextH1 = h1Elements[currentH1Index + 1];
            window.scrollTo(0, nextH1.offsetTop);
        }
    }
    

    I have learnt more in this short time, than days hunting with search engines. A combination of helpful humans and a good LLM is very effective.

    Many thanks