The new 1Writer with Javascript actions

While you were stuffing a turkey with some delicious spices, 1Writer got a juicy new update up in the App Store. New Javascript actions, Touch ID support, in-app browser with 1Password integration, Safari share extension and more.

Javascript Actions

Editorial showed how a programming language can enhance the experience for a text editor. Drafts came next with support for Javascript and allowed us to create great actions with it.

1Writer now pairs with Drafts on Javascript with 5 unique objects:

  • editor: Manipulates the content of the editor; can also open and close files. Some methods are not available in the share extension;
  • app: Mostly important for clipboard interaction, can also toggle the dark theme and a few more tricks;
  • ui: Support for user interaction. Alerts, inputs, HUD messages, you name it. This object's methods are asynchronous, more on that later;
  • http: Performs HTTP requests, in a nutshell this means you can make API calls in your actions. Also asynchronous;
  • webBrowser: Interacts with 1Writer's built-in browser, including to load custom HTML.

You can read the entire documentation here. These objects pack some punch, specially the http object, but we'll play our first move on a simpler script. Writing long articles can be overwhelming, specially when you need to find your way back to a specific section, so I wrote a script that prompts you with all the headings of the current document and jumps to the one you select.

Jump to Section:

var content, lines, headings, lengths, ranges, linesLength, i;

content = editor.getText();
lines = content.split('\n');
linesLength = lines.length;
lengths = 0;
headings = Array();
ranges = Array();

for(i = 0; i < linesLength; i++) {
    if(lines[i].match(/^#+\s?.*/)) {
        headings.push(lines[i]);
        ranges.push(lengths);
    }
    lengths += lines[i].length + 1; // +1 because the line breaks count;
}

if (headings.length > 0) {
    ui.list('Jump to Section', headings, false, function(a,b) {
        if (b === undefined) {
            ui.hudError("Don't jump then");
        } else {
            editor.setSelectedRange(ranges[b])
        }
    });
} else {
    ui.hudError("There's no heading");
}

How this works? First we collect and split the whole content in lines, then we loop through each line and see if it matches a Markdown header (starts with #), we push each positive match to the headings array.

The tricky bit comes to getting the right cursor position since you can't arbitrarily search and place the cursor in Javascript. You can position the cursor by setting a selection range without an end, therefore it would be 0.

We find out the correct position for each heading by checking the length of every line and adding to the lengths variable. It changes while we loop, while for the first line it will be 0, for the second line it will be the length of the first line plus 1 (because line breaks count).

If there's any heading, the script will prompt the user with a list, otherwise, will show a HUD error. Then we check if the user actually selected a heading from the list, if not, shows a different HUD error. If there's a selected heading, we jump to that section.

May take a moment or two to wrap your head around this one, but I guarantee it is a simple action and a good starting point to Javascript in 1Writer. Next we're moving to HTTP requests and things will get trickier.

Handling HTTP Requests

1Writer 2.0 brings a powerful feature for automation in HTTP requests, in short terms, it means you can communicate with web applications to retrieve data. However, before jumping into the script editor, we must learn how an asynchronous function works.

All the methods from the http and the ui module run asynchronously, that means the script won't wait for its completion to resolve the next line. I'll exemplify that with a script to set the document's text to the output from an user input.

var input;
input = ui.input('Write some text', function(value) { return value});
editor.setText(input);

What you would expect from this script? That the user writes some text in the prompt and the callback function returns this data, assigning it to the input variable and only then setting the document to it. However, since the ui object is asynchronous, the editor will set the text to nothing (since input has no defined value) while the input is still up. Therefore, you also can't assign these outcomes to variables.

We work around that like this:

var input;
ui.input('Write some text', function(value) { editor.setText(value); });

This is a solution for simple issues, more complex scripts would require multiple named functions to move the script in the correct pace. So we'll dive deeper into the structure of HTTP requests using a script sent to me by Ngoc Luu himself to search the App Store. It may look complicated, but we'll split it in bits to explain.

ui.input('App Name', '', 'Enter app name', searchApp);

Prompts the user and runs the named function searchApp.

function searchApp(appName) {
    if (!appName) { //user pressed Cancel button
        return;
    }
    ui.hudProgress('Searching');
    http.get('https://itunes.apple.com/search', { term: appName, media: 'software', country: 'us', entity: 'software' }, handleResponseData);
}

Which checks if the user didn't cancel the input (if so, returns), otherwise it sends the HTTP request to iTunes. Notice how the first item is the URL; the second is a dictionary with the data we send to the server, like the name of the app and the country. It runs the handleResponseData function with the iTunes response.

function handleResponseData(response, error) {
    if (error) {
        ui.hudError();
        return;
    }
    ui.hudDismiss();
    var listData = response.results.map(function(item) {
        //this value will be inserted into the document
        var value = item.trackName + ' - [*' + item.formattedPrice + '*](' + item.trackViewUrl + ')';
        return item.trackName + '|' + value + '|' + item.formattedPrice;
    });
    ui.list('Search Result', listData, true, insertAppInfo);
}

The handleResponseData function checks if iTunes sent an error, shows a HUD if so, otherwise, maps an array with the data retrieved. Notice how the variable value contains the whole link, yet it returns a more robust element to complement the list formatting. This is a different approach than the multiple arrays I used in the Jump to Section script, because when the user selects an item, it will pass the value to the handler function. Also, the ui.list has a true parameter, indicating you can select multiple items before triggering the insertAppInfo function.

function insertAppInfo(selectedValues) {
    if (!selectedValues) { //user pressed Cancel
        return;
    }
    var text = selectedValues.join('\n');
    if (editor.isClosed()) { //create new file if needed
        editor.newFile(text);
    }
    else {
        editor.replaceSelection(text);
    }
}

The last function picks the apps selected, converts them to a string, if there's no file open, it creates a new one, otherwise, it replaces the current selection with the apps you picked. If you drifted along the way, here's a flowchart:

How the Get App Link works?
How the Get App Link script works.

As soon as you comprehend how these asynchronous requests work, everything gets easy (as long as you know some Javascript, of course) and you may bend 1Writer to your will, as in, for example, requesting your latest Pinboard bookmarks, getting the current weather or collecting information about a movie. You can also stick around for more actions and scripts, of course.

Extensions

1Writer has one of the most useful extra keyboard rows out there and now you can set shortcuts to actions in its keys. Just tap and hold on an editable key (they have a tiny triangle in the upper-right corner), choose Set Action... and select an action you want.

Then there's the Safari extension. Just as Drafts, you create a template for capture in the app settings, however, 1Writer allows you to trigger actions from the extension, so you don't even need to create a new file to run actions through web content.

As Good as Apple Pie

1Writer 2.0 was the most delightful surprise this week and probably the best deal you could make this holiday if you write in your iOS devices. I'm sure it is only a matter of time until more actions pop up and improve the pool of options in this outstanding text editor.