JXA: need help with a few things

Hi there,

I have some little questions, as simple as:

How do I create a new note in Javascript?
How do I open and close a specific document in Javascript?

Let’s assume, I want to open a file Test.tbx, which is located in the folder ~/Documents (Do I need the path at all?). Then I want to simply create a new note with the name “Testnote” and the text “Testtext” on the top level of the document. If possible, as the first child.

Then close the document again.

Thanks a lot in advance

Arggghhhh…

After some hours I found my first error:

let newNote = tb.Note({name: "Test"})

I simply had to capitalize the N of the Note (tb.Note instead of tb.note) . These are the little things, that drive me crazy :crazy_face:

Ok, but there are some other question open…

Using a pre-fab filePath function to expand the ~ to a full path prefix, we might write, for example:

(() => {
    'use strict';

    const main = () => {

        // DECLARATIONS
        const
            tinderbox8 = Application('Tinderbox 8'),

            // '~'' expanded to full path.
            fp = filePath('~/Documents/test.tbx'),

            newNote = new tinderbox8.Note({
                name: 'Some title for a new note'
            });

        // EFFECTS
        tinderbox8.open(fp);
        tinderbox8.activate();
        tinderbox8.documents[0].notes.push(newNote);

        // RETURN VALUE
        return newNote.name();
    };

    // GENERIC FUNCTIONS ----------------------------
    // https://github.com/RobTrew/prelude-jxa

    // filePath :: String -> FilePath
    const filePath = s =>
        ObjC.unwrap(ObjC.wrap(s)
            .stringByStandardizingPath);

    // CALL MAIN FUNCTION
    return main();
})();

Let me know which bits might need clarifying first : -)

Absolutely – it’s the general pattern of JavaScript that :

  • the name of a Class of objects is upper-cased,
  • while the name of a particular object starts in lower case.

Haha :joy:

If you ask me like this: All bits need clarifying!!!

I don’t understand the whole construct, that you are using. It seems reasonable, but I don’t get it. For example, it seems like every of your scripts is a big arrow function. Why? What is the advantage? Who is calling the function? And so on and so on…
I think, it also could be done easier, for a noob like me.
But perhaps I will understand it some day. I will try.

Btw, I already cloned your repo (this prelude jxa thing) and played a little bit around. Don’t understand much, but it looks interesting :joy:

(I feel somehow, that this will simplify things at the end… but only at the end…)

Thanks so far…

Starting from the outside, the first thing to be aware of is that Script Editor runs your JS in a global environment which persists between script runs. In other words, names given to things by one script may be still be lying around, and produce unwanted and unexpected effects, in the next script that you run.

This, for example, works fine the first time we run it in Script Editor:

const x = 2

x + x

and returns the value 4.

But now experiment: what happens if we press the run button a second time ?

Error -2700: Script error.

In Script Editor’s global environment, the constant x was still defined, and the second run of the script is making an illegal request to redefine it.

There is a simple solution – to avoid any global name-defining in our scripts, and make everything local – forgotten when the script returns its value and ends.

The trick is this:

(() => {
    'use strict';

    // Do everything in this purely local space, 
    // inside the definition of a nameless function, 
    // which is immediately executed because of the ( ) at the end.

})(); 

(I start every script with Typinator snippet which expands to this template)

So now try this, as many times as you like, using the return keyword to return a value from the plain vanilla ‘anonymous function’.

There will be no trace left behind in the JS ‘global namespace’, and there will be no run-time error.

This is the simplest way to run JXA JavaScripts.

(() => {
    'use strict';

    const x = 2;
    
    return x + x;

})();

If you want to do a little more typing, you could rewrite it as follows, but you get no benefits for the extra visual noise and typing effort :slight_smile:

(function () {
    'use strict';

    const x = 2;
	
    return x + x;
})();

Any other puzzle that seems particularly prominent ?

1 Like

PS You can certainly skip the 'use strict'; incantation, but if you do, the error messages that you are shown will be less informative, and you will be left more puzzled if anything goes unexpectedly.

Your script lets Tinderbox panic. Is there something missing? Perhaps because of the Objective-C part? Do you need to import anything?

Ah, and one thing, that happened: I had Test.tbx already open and another document too, and the new note was written to the wrong document. So how could you set the focus to the right document?

This is really cool!

1 Like

We can specify a document by name:

tinderbox8.documents['test.tbx'].notes.push(newNote)

(See in full below)

Tell me more about the ‘panic’ – what error were you getting ?

(I’m sure you’ve checked for the most common problem in these contexts, which often affects me too – a copy paste which has accidentally dropped some tiny part from start or end of the posted script)

What version of Tbx and macOS are you running ?

This works here:


(() => {
    'use strict';

    const main = () => {

        // DECLARATIONS
        const
            tinderbox8 = Application('Tinderbox 8'),

            // '~'' expanded to full path.
            fp = filePath('~/Documents/test.tbx'),

            newNote = new tinderbox8.Note({
                name: 'Some title for a new note'
            });

        // EFFECTS
        tinderbox8.open(fp);
        tinderbox8.activate();
        tinderbox8.documents['test.tbx'].notes.push(newNote);

        // RETURN VALUE
        return newNote.name();
    };

    // GENERIC FUNCTIONS ----------------------------
    // https://github.com/RobTrew/prelude-jxa

    // filePath :: String -> FilePath
    const filePath = s =>
        ObjC.unwrap(ObjC.wrap(s)
            .stringByStandardizingPath);

    // CALL MAIN FUNCTION
    return main();
})();

The good old spinning pizza…
Event: hang

Mac OS X 10.14.5 (Build 18F132)
8.0.7 (b391)

And still with the second version of the script, and no lines missing ?

Hard to reproduce here. The trick is probably to comment most of it out, see how much, if anything, you can run of the opening lines without triggering and then expand to the first point where a problem occurs.

(if you can get a system log with relevant looking entries that might yield a clue)

(I’m running the same TBX as you, on macOS 10.14.6, but I don’t think that the OS version is likely to be relevant).

PS is your test.tbx large ? I wondered if we were catching it mid load, before it was able to respond to the API. Could be that we need to ensure that the file is fully parsed before moving on to the note creation and its addition to the notes collection.

(Mine is just an empty file)

By the way: 8.0.7 is a backstage build; we generally don’t talk about those on the main forum, as that can confuse people. No harm here, though.

https://www.eastgate.com/Tinderbox/Backstage.html

It was indeed an issue of the Backstage version.
Sorry for the confusion… (@all, who are confused… I was not aware of this, as I mostly only use the latest version)

I think the hang occurs only in an unreleased test version; we’ll watch out for it.

@ComplexPoint: Do you have an idea, how this can be achieved?

I think you should replace

    tinderbox8.documents['xsmart'].notes.push(newNote);

with

   tinderbox8.documents['xsmart'].unshift(newNote);

But it’s been a while since I’ve used Javascript for anything like this.

That was my first idea, but somehow didn’t do the trick.

The thing to be aware of with collections like notes, is that Apple has created some syntactic sugar to make them look a little like JS arrays, but they are really osascript objects.

documents[0] gets pre-preprocessed to the real underlying
documents.at(0)

and
documents['text.tbx'] gets rewritten to
documents.byName('text.tbx')

before the JS interpreter itself actually sees them.

(I personally just use .at(n) and .byName(nameString), finding them slightly less confusing, and even fractionally faster at the last time of testing, though not perceptibly)

Meanwhile the .push and .unshift methods on that object are not really the JS Array methods which they look like, and don’t behave the same way.

We can obtain an actual Array of note values by writing:

documents['text.tbx'].byName('text.tbx').notes()

The trailling ( ) there creates an array. Without it, a plain

documents['text.tbx'].byName('text.tbx').notes

Is a reference to a function object.

(Manipulating an Array of note values, doesn’t however, make any changes to the order of the actual collection of notes in the app)

The note moving pattern is limited, as far as I can see, (in AS and JS) to moving an item into a new container.

So, for example, we could create a new note (the special collection.push() method always appends), and then move it from the end of its current container to become a child of some other note.

Here, we move the freshly created note to become a child of the first note in the document:

(() => {
    'use strict';

    const main = () => {

        // DECLARATIONS
        const
            tinderbox8 = Application('Tinderbox 8'),

            // '~'' expanded to full path.
            fp = filePath('~/Documents/test.tbx'),
            newNote = new tinderbox8.Note({
                name: 'an additional note of some kind'
            });

        // EFFECTS
        tinderbox8.open(fp);
        tinderbox8.activate();

        const
            doc = tinderbox8.documents.byName('test.tbx'),
            notes = doc.notes; // Collection reference, not a JS array.

        notes.push(newNote); // A special 'push', not the Array.push() method.
        tinderbox8.move(
            notes.at(-1), {
                to: notes.at(0)
            }
        );
    };

    // GENERIC FUNCTIONS ----------------------------
    // https://github.com/RobTrew/prelude-jxa

    // filePath :: String -> FilePath
    const filePath = s =>
        ObjC.unwrap(ObjC.wrap(s)
            .stringByStandardizingPath);

    // CALL MAIN FUNCTION
    return main();
})();

Sounds a little bit strange. That would mean, that it is really not possible to move the notes around in the top level? But only move them to another note as a child?