Search Field Mega Action

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

1 Like

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();
})();
1 Like

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

1 Like

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();
})();
1 Like

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()
        )
    )
)

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.

1 Like

Can anyone suggest a hack to make this open mobile Safari, rather than use the in-app browser? I realize there’s an option to switch to Safari once the in-app browser opens, but I’d like to cut out the extra step.

This is only an issue re: iOS. In Drafts-for-desktop, the action does open Safari.

Change the second parameter of openURL to false.

See the scripting docs here - https://scripting.getdrafts.com/classes/app#openurl

Thanks, I never imagined it would be so easy!

I’m going to keep tweaking this script as a way to teach myself…

Below is the same script as jmreekes’, but with lots more searches added. Like the original script, it launches an in-app browser in mobile (not Safari). I’ve listed the sites for convenience above the script:

Amazon
Amazon Orders
AppStore
BIu-Ray.com
DuckDuckGo
eBay Completed
eBay
Facebook
Forvo Pronunciation
Google
Google Images
GoogleTranslate
IMDB
iTunes
JustWatch.com
Macupdate
Movielens
MRQE
Netflix
Pinboard
RottenTomatoes
Translate English -> Portuguese
Translate English -> Spanish
Translate Portuguese -> English
Translate Spanish -> English
Twitter
Twitter Advanced Search (Rick Wilson)
Urban Dictionary
Wikipedia
WolframAlpha
Yelp (zip code 10001)
YouTube

//Mega Multi Web Search 
/* Action created by Jimmy Reekes, based on actions created by Tim Nahumck */

var content = draft.processTemplate("[[selection]]")

var p = Prompt.create();
p.title = "Select Search Action";
p.addTextField("search", "Search", content);
var actions = ["Amazon","Amazon Orders","AppStore","BIu-Ray.com","DuckDuckGo","eBay Completed","eBay","Facebook","Forvo Pronunciation","Google","GoogleImages","GoogleTranslate","IMDB","iTunes","Jim Leff's Slog","JustWatch.com","Macupdate","Movielens","MRQE","Netflix","Pinboard","RottenTomatoes","Sepinwall Rolling Stone","Translate English -> Portuguese","Translate English -> Spanish","Translate Portuguese -> English","Translate Spanish -> English","Twitter","Twitter Advanced Search (Rick Wilson)","Twitter Must-Read","Urban Dictionary","Wikipedia","WolframAlpha","Yelp (10001)","Yelp (10570)","YouTube"];
for (action of actions) {
  p.addButton(action);
}
var con = p.show();
//Actions based on button presses
if (con) {
  var input = p.fieldValues["search"];
  var output = encodeURIComponent(input);
	var state = "true";
	if (p.buttonPressed == "Amazon") {
		var url = "http://amazon.com/s?ie=UTF8&index=blended&keywords="+output;
	}
	if (p.buttonPressed == "Amazon Orders") {
		var url = "https://www.amazon.com/gp/your-account/order-history/ref=ppx_yo_dt_b_search?opt=ab&search="+output;
	}
	if (p.buttonPressed == "AppStore") {
		var url = "itms-apps://search.itunes.apple.com/WebObjects/MZSearch.woa/wa/search?media=software&term="+output;
		var state = "";
	}
	if (p.buttonPressed == "BIu-Ray.com") {
		var url = "https://www.blu-ray.com/search/?quicksearch=1&quicksearch_country=US&quicksearch_keyword="+output;
	}
	if (p.buttonPressed == "DuckDuckGo"){
		var url = "https://duckduckgo.com/?q="+output;

	}
	if (p.buttonPressed == "eBay") {
		var url = "https://www.ebay.com/sch/i.html?_nkw="+output;
	}
	if (p.buttonPressed == "eBay Completed") {
		var url = "https://www.ebay.com/sch/i.html?_from=R40&_sacat=0&rt=nc&LH_Complete=1&_nkw="+output;
	}
	if (p.buttonPressed == "Facebook") {
		var url = "https://www.facebook.com/search/top/?q="+output;
	}
	if (p.buttonPressed == "Forvo Pronunciation") {
		var url = "https://forvo.com/search/"+output;
	}
		if (p.buttonPressed == "Google") {
		var url = "https://google.com/search?q="+output;
	}
	if (p.buttonPressed == "GoogleImages") {
		var url = "https://google.com/search?tbm=isch&q="+output;
	}
	if (p.buttonPressed == "GoogleTranslate") {
		var url = "http://translate.google.com/#auto/en/"+output;
	}
	if (p.buttonPressed == "IMDB") {
		var url = "https://www.imdb.com/find?q="+output;
	}
	if (p.buttonPressed == "iTunes") {
		var url = "itms:/search?term="+output;
		var state = "false";
	}
	if (p.buttonPressed == "Jim Leff's Slog") {
		var url = "https://jimleff.blogspot.com/search?q="+output;
	}
	if (p.buttonPressed == "JustWatch.com") {
		var url = "https://www.justwatch.com/us/search?q="+output;
	}
	if (p.buttonPressed == "Macupdate") {
		var url = "https://www.macupdate.com/find/mac/context%3D"+output;
	}
	if (p.buttonPressed == "Movielens") {
		var url = "https://twitter.com/search?%20list%3A834895775314415616&src=typed_query&f=live&q="+output;
	}
	if (p.buttonPressed == "MRQE") {
		var url = "https://www.mrqe.com/search?utf8=✓&q="+output;
	}
	if (p.buttonPressed == "Netflix") {
		var url = "https://www.netflix.com/search?q="+output;
	}
	if (p.buttonPressed == "Pinboard") {
		var url = "https://pinboard.in/search/u:Jimbo?query="+output;
	}
	if (p.buttonPressed == "RottenTomatoes") {
		var url = "http://www.rottentomatoes.com/search/?search="+output;
	}
	if (p.buttonPressed == "Sepinwall Rolling Stone") {
		var url = "https://www.rollingstone.com/results/#?q=sepinwall%20"+output;
	}
	if (p.buttonPressed == "Translate English -> Portuguese") {
		var url = "https://translate.google.com/#view=home&op=translate&sl=en&tl=pt&text="+output;
	}
	if (p.buttonPressed == "Translate English -> Spanish") {
		var url = "https://translate.google.com/#view=home&op=translate&sl=en&tl=es&text="+output;
	}
	if (p.buttonPressed == "Translate Portuguese -> English") {
		var url = "https://translate.google.com/#view=home&op=translate&sl=pt&tl=en&text="+output;
	}
	if (p.buttonPressed == "Translate Spanish -> English") {
		var url = "https://translate.google.com/#view=home&op=translate&sl=es&tl=en&text="+output;
	}
	if (p.buttonPressed == "Twitter") {
		var url = "https://twitter.com/search?q="+output;
		var state = "";
	}
	if (p.buttonPressed == "Twitter Advanced Search - Rick Wilson") {
		var url = 
"https://twitter.com/search?q="+output;
		var state = "";
	}
	if (p.buttonPressed == "Twitter Must-Read") {
		var url = "https://twitter.com/search?q=list%3A834895775314415616%20"+output;
	}
	if (p.buttonPressed == "Urban Dictionary") {
		var url = "https://www.urbandictionary.com/define.php?term="+output;
	}
	if (p.buttonPressed == "Wikipedia") {
		var url = "https://en.wikipedia.org/wiki/Special:Search?search="+output;
	}
	if (p.buttonPressed == "WolframAlpha") {
		var url = "http://wolframalpha.com/input/?i="+output;
	}
	if (p.buttonPressed == "Yelp (10570)") {
		var url = "https://www.yelp.com/search?find_loc=10570&find_desc="+output;
	}
	if (p.buttonPressed == "Yelp (10001)") {
		var url = "https://www.yelp.com/search?find_loc=10001&find_desc="+output;
	}
	if (p.buttonPressed == "YouTube") {
		var url = "http://www.youtube.com/results?search_query="+output;
		var state = ""
	}
	var result = app.openURL(url, state);
}
else {
	context.cancel();
}

One problem with my script, just above. One search actually wants two parameters: text string, plus Twitter account to search via their advanced search. It’s currently configured to one arbitrary Twitter account. Can anyone come up with a way to input both parameters for only this particular search?

Here’s how I have it presently:

	if (p.buttonPressed == "Twitter Advanced Search - Rick Wilson") {
		var url = 
"https://twitter.com/search?q="+output;
		var state = "";