Search Field Mega Action


#1

This is a new version of my Search Mega Action, that adds a text field to the prompt that will either be pre-filled with the draft, the selected text, or be blank. This means the action can be run on a blank draft without creating a new draft. I used bits of script from an action by @nahumck, who also helped clean the code up in the original action. https://actions.getdrafts.com/a/1Jl


#2

These lists of services are a good idea.

One way of making it slightly easier and quicker to edit the list might be to drop:

  1. the structure of if ( ... ) { assignment } conditions, and
  2. the need to name a service in two different places (actions list, and repeated buttonPressed conditions);

replacing these with a single list of (name, url) pairs, like:

// services :: [(String, String)]
const services = [
    [
        'Amazon',
        'http://amazon.com/s?ie=UTF8&index=blended&keywords='
    ],
    [
        'AppStore',
        'itms-apps://search.itunes.apple.com/WebObjects/' +
        'MZSearch.woa/wa/search?media=software&term='
    ],
    [
        'DuckDuckGo',
        'https://duckduckgo.com/?q='
    ],
    [
        'Google',
        'https://google.com/search?q='
    ],
    [
        'GoogleImages',
        'https://google.com/search?tbm=isch&q='
    ],
    [
        'GoogleTranslate',
        'http://translate.google.com/#auto/en/'
    ],
    [
        'IMDB',
        'https://www.imdb.com/find?q='
    ],
    [
        'iTunes',
        'itms:/search?term='
    ],
    [
        'RottenTomatoes',
        'http://www.rottentomatoes.com/search/?search='
    ],
    [
        'Twitter',
        'https://twitter.com/search?q='
    ],
    [
        'Wikipedia',
        'https://en.wikipedia.org/wiki/Special:Search?search='
    ],
    [
        'WolframAlpha',
        'http://wolframalpha.com/input/?i='
    ],
    [
        'YouTube',
        'http://www.youtube.com/results?search_query='
    ]
];

A list of pairs like this can be automatically converted to a JS Map object.

const serviceMap = new Map(services);

which gives us short-cut methods like .keys(), .values(), and .get()

So if we use a generic menuChoice function like:

// AN ITEM CHOSEN FROM A MENU

// menuChoice :: Bool -> String -> String -> [String] -> String
const menuChoice = (withCancelButton, title, msg, items) => {
    const p = items.reduce(
        (a, item) => (a.addButton(item), a),
        Object.assign(
            Prompt.create(), {
                title: title,
                message: msg,
                isCancellable: withCancelButton
            }
        )
    );
    return items.length > 0 ? (
        p.show() ? p.buttonPressed : 'Cancel'
    ) : 'Empty menu';
};

We can immediately .get() the url matching a chosen service name:

const maybeURL = serviceMap.get(
    menuChoice(
        true,
        'Search on chosen web service',
        'Choose:', [...serviceMap.keys()]
    )
)

(If the user cancelled, then the value of maybeURL will be the special value: undefined)

So for a less ambitious variant of your action, in which we just submit any selected text to the web search service, we can then write:

return maybeURL !== undefined ? (
    app.openURL(
        maybeURL + encodeURIComponent(

            // Selected text (if any)
            e.getSelectedText().trim() ||

            // otherwise line containing cursor.
            e.getTextInRange(
                ...e.getSelectedLineRange()
            )
        )
    )
) : console.log('User cancelled ...');

i.e. in full, something along the lines of:

(() => {
    'use strict';

    const main = () => {
        const
            e = editor,
            serviceMap = new Map(services),
            maybeURL = serviceMap.get(
                menuChoice(
                    true,
                    'Search on chosen web service',
                    'Choose:', [...serviceMap.keys()]
                )
            );
        return maybeURL !== undefined ? (
            app.openURL(
                maybeURL + encodeURIComponent(

                    // Selected text (if any)
                    e.getSelectedText().trim() ||

                    // otherwise line containing cursor.
                    e.getTextInRange(
                        ...e.getSelectedLineRange()
                    )
                )
            )
        ) : console.log('User cancelled ...');
    };

    // services :: [(String, String)]
    const services = [
        [
            'Amazon',
            'http://amazon.com/s?ie=UTF8&index=blended&keywords='
        ],
        [
            'AppStore',
            'itms-apps://search.itunes.apple.com/WebObjects/' +
            'MZSearch.woa/wa/search?media=software&term='
        ],
        [
            'DuckDuckGo',
            'https://duckduckgo.com/?q='
        ],
        [
            'Google',
            'https://google.com/search?q='
        ],
        [
            'GoogleImages',
            'https://google.com/search?tbm=isch&q='
        ],
        [
            'GoogleTranslate',
            'http://translate.google.com/#auto/en/'
        ],
        [
            'IMDB',
            'https://www.imdb.com/find?q='
        ],
        [
            'iTunes',
            'itms:/search?term='
        ],
        [
            'RottenTomatoes',
            'http://www.rottentomatoes.com/search/?search='
        ],
        [
            'Twitter',
            'https://twitter.com/search?q='
        ],
        [
            'Wikipedia',
            'https://en.wikipedia.org/wiki/Special:Search?search='
        ],
        [
            'WolframAlpha',
            'http://wolframalpha.com/input/?i='
        ],
        [
            'YouTube',
            'http://www.youtube.com/results?search_query='
        ]
    ];

    // AN ITEM CHOSEN FROM A MENU

    // menuChoice :: Bool -> String -> String -> [String] -> String
    const menuChoice = (withCancelButton, title, msg, items) => {
        const p = items.reduce(
            (a, item) => (a.addButton(item), a),
            Object.assign(
                Prompt.create(), {
                    title: title,
                    message: msg,
                    isCancellable: withCancelButton
                }
            )
        );
        return items.length > 0 ? (
            p.show() ? p.buttonPressed : 'Cancel'
        ) : 'Empty menu';
    };

    // MAIN ---
    return main();
})();

#3

Cool, and thanks. I’m really, really new to JavaScript, so I’m still learning.


#4

Here, FWIW, is a variant of your action which aims for a bit of extra flexibility by taking a different approach:

  • removing the list of services from the code of the script step,
  • placing it in a draft (in markdown link format) and
  • giving the script step just the UUID of the draft which contains the list.

(We can get the UUID of a draft through the information icon at top left – the information panel has a Copy Link to Draft control)

The draft contents might look something like this (based on your action, and making just a few small edits to ensure that they are all https://, rather than the less secure http://)

[Amazon](https://amazon.com/s?ie=UTF8&index=blended&keywords=)
[App Store](itms-apps://search.itunes.apple.com/WebObjects/MZSearch.woa/wa/search?media=software&term=)
[DuckDuckGo](https://duckduckgo.com/?q=)
[Google](https://google.com/search?q=)
[Google Images](https://google.com/search?tbm=isch&q=)
[Google Translate](https://translate.google.com/#auto/en/)
[IMDB](https://www.imdb.com/find?q=)
[iTunes](itms:/search?term=)
[Rotten Tomatoes](https://www.rottentomatoes.com/search/?search=)
[Twitter](https://twitter.com/search?q=)
[Wikipedia](https://en.wikipedia.org/wiki/Special:Search?search=)
[Wolfram Alpha](https://www.wolframalpha.com/input/?i=)
[YouTube](https://www.youtube.com/results?search_query=)

JS source, which can be amended to contain a UUID from a draft on the user’s device:

http://actions.getdrafts.com/a/1Jq

(() => {
    'use strict';

    /* Edit to provide UUID of a draft containing MD links with
      named search expressions up to the '=' sign.
      e.g.

         [DuckDuckGo](https://duckduckgo.com/?q=)
   */
    // SERVICES EITHER FROM MARKDOWN LINKS IN THIS DRAFT:
    const uuidLinksDraft =
        '9A15F571-0871-4DF7-A1F3-2E5464F197A3';

    // OR IF DRAFT NOT FOUND BY THAT UUID, THEN FROM THESE DEFAULTS:
    const fallbackDefaults = `
[Amazon](https://amazon.com/s?ie=UTF8&index=blended&keywords=)
[App Store](itms-apps://search.itunes.apple.com/WebObjects/MZSearch.woa/wa/search?media=software&term=)
[DuckDuckGo](https://duckduckgo.com/?q=)
[Google](https://google.com/search?q=)
[Google Images](https://google.com/search?tbm=isch&q=)
[Google Translate](https://translate.google.com/#auto/en/)
[IMDB](https://www.imdb.com/find?q=)
[iTunes](itms:/search?term=)
[Rotten Tomatoes](https://www.rottentomatoes.com/search/?search=)
[Twitter](https://twitter.com/search?q=)
[Wikipedia](https://en.wikipedia.org/wiki/Special:Search?search=)
[Wolfram Alpha](https://www.wolframalpha.com/input/?i=)
[YouTube](https://www.youtube.com/results?search_query=)`;

    const main = () => {
        const
            d = Draft.find(uuidLinksDraft),
            lrResult = bindLR(
                bindLR(
                    // LIST OF SERVICES EITHER FROM DRAFT
                    // WITH GIVEN UUID OR FROM DEFAULTS ABOVE
                    Right(
                        d !== undefined ? [
                            'List found in draft:\n' +
                            uuidLinksDraft,
                            d.content
                        ] : [
                            'No draft found by uuid:\n' +
                            uuidLinksDraft +
                            '\n(using default service list)',
                            fallbackDefaults
                        ]
                    ),
                    ([sourceName, strLinks]) => Right([
                        sourceName,
                        new Map(
                            regexMatches(
                                /\[(.*)\]\((.*)\)/g,
                                strLinks
                            )
                            .map(x => [x[1], x[2]])
                        )
                    ])
                ),
                ([sourceName, serviceMap]) => {
                    const e = editor;
                    return bindLR(
                        textAndMenuChoiceLR(
                            true, 'Search with chosen service',
                            sourceName,
                            'Term:',

                            // SELECTION OR CURRENT LINE
                            e.getSelectedText().trim() ||
                            e.getTextInRange(
                                ...e.getSelectedLineRange()
                            ), [...serviceMap.keys()]
                        ),
                        dct => {
                            const maybeURL = serviceMap
                                .get(dct.choice);
                            return maybeURL !== undefined ? (
                                Right(
                                    app.openURL(
                                        maybeURL +
                                        encodeURIComponent(
                                            dct.text
                                        )
                                    )
                                )
                            ) : Left(
                                'Service not found: ' +
                                dct.choice
                            );
                        }
                    );
                }
            );
        const strLeft = lrResult.Left;
        return strLeft && strLeft.includes('uuid') ? (
            alert(strLeft)
        ) : console.log(lrResult.Left || lrResult.Right)
    };

    // A CONFIRMED TEXT, AND AN ITEM CHOSEN FROM A MENU

    // textAndMenuChoiceLR :: Bool -> String -> String ->
    //    String -> String -> [String] ->
    //  Either String { text: String, choice :: String }
    const textAndMenuChoiceLR = (
        withCancelButton, title, msg,
        strLabel, strText, items) => {
        const p = items.reduce(
            (a, item) => (a.addButton(item), a),
            Object.assign(
                Prompt.create(), {
                    title: title,
                    message: msg,
                    isCancellable: withCancelButton
                }
            )
        );
        return (
            p.addTextField('txt', strLabel, strText),
            items.length > 0 ? (
                p.show() ? Right({
                    text: p.fieldValues.txt,
                    choice: p.buttonPressed
                }) : Left('Cancel')
            ) : Left('Empty menu')
        );
    };

    // GENERIC FUNCTIONS ---

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });

    // Right :: b -> Either a b
    const Right = x => ({
        type: 'Either',
        Right: x
    });

    // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
    const bindLR = (m, mf) =>
        m.Right !== undefined ? (
            mf(m.Right)
        ) : m;

    // regexMatches :: String -> String -> [[String]]
    const regexMatches = (strRgx, strHay) => {
        const rgx = new RegExp(strRgx, 'g');
        let m = rgx.exec(strHay),
            xs = [];
        while (m)(xs.push(m), m = rgx.exec(strHay));
        return xs;
    };

    // MAIN ---
    return main();
})();

#5

PS to ease the burden of selecting some text, we can fall back, if no selection is found, to the text of the line which contains the cursor:

const e = editor;

// ...

app.openURL(
    maybeURL + encodeURIComponent(

        // Selected text (if any)
        e.getSelectedText().trim() ||

        // otherwise line containing cursor.
        e.getTextInRange(
            ...e.getSelectedLineRange()
        )
    )
)

#6

I had written something similar that reads titles & urls from a csv. It occurred to me that this approach would allow a degree of customisation of workflows for non-subscribers.