Bug: Can't paste text into a Numbers' cell

For some reason I can’t paste any text inside a Numbers app cell. It works when I paste while a cell is just selected, but not when the cursor is inside a cell (after a double click). ‘Edit’ in menu bar does blink, just nothing else happens. ‘Paste and Match Style’ doesn’t work either.

Is this a question about Bike ?

You have copied text in Bike and want to paste it into a cell of a Numbers.app spreadsheet ?


The choice of what to paste is made by the receiving application, and Numbers looks as if it has a buggy handling of clipboards which contain multiple formats.

(Perhaps it sees the HTML and OPML outlines and then gives up before finding the plain text component).

Two approaches in the meanwhile:

  1. You could paste into something else and copy again before pasting into Numbers.
  2. We could write you a script to Copy As plain text only, so that Numbers doesn’t fail.

(I notice that Microsoft Excel’s paste handler works fine with Bike clipboard contents)


Do you happen to have anything like Keyboard Maestro or FastScripts ?

Yes, it’s about Bike. This is the first time I ever had issues pasting text into Numbers, so I assumed it’s a Bike bug.

Currently, I do paste from Bike into Spotlight first, but it’s clunky, so I would appreciate a script. I use Spark (https://www.shadowlab.org/Software/spark.php) that allows assigning AppleScript execution to a hotkey.

OK, here’s basic core of a script, which you can test in Script Editor.app (with the language selector at top left set to JavaScript, rather than AppleScript)

Using Scripts - Bike

It converts the clipboard (after you have copied from Bike), into a plain text only format, to make life simpler for Numbers.app.

We could embed it in something which also starts by running Edit > Copy in Bike, to improve the workflow, but why don’t you test this first:

Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    // Convert clipboard to Plain text only.

    // To get around a bug in Numbers.app,
    // which seems to stops looking for plain text when
    // it sees HTML and OPML outlines in the clipboard

    ObjC.import("AppKit");

    const main = () =>
        fmapLR(
            copyText
        )(
            clipTextLR()
        );

    // ----------------------- JXA -----------------------

    // clipTextLR :: () -> Either String String
    const clipTextLR = () => {
        // Either a message, (if no clip text is found),
        // or the string contents of the clipboard.
        const
            v = ObjC.unwrap(
                $.NSPasteboard.generalPasteboard
                .stringForType($.NSPasteboardTypeString)
            );

        return Boolean(v) && 0 < v.length ? (
            Right(v)
        ) : Left("No utf8-plain-text found in clipboard.");
    };


    // copyText :: String -> IO String
    const copyText = s => {
        const pb = $.NSPasteboard.generalPasteboard;

        return (
            pb.clearContents,
            pb.setStringForType(
                $(s),
                $.NSPasteboardTypeString
            ),
            s
        );
    };

    // --------------------- GENERIC ---------------------

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


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

    // fmapLR (<$>) :: (b -> c) -> Either a b -> Either a c
    const fmapLR = f =>
        // Either f mapped into the contents of any Right
        // value in e, or e unchanged if is a Left value.
        e => "Left" in e ? (
            e
        ) : Right(f(e.Right));

    return main();
})();
1 Like

If the script above works, then try this, which is a first draft of an

Edit > Copy as Plain Text only

for Bike.

If it works from Script Editor, then you could try assigning it to a hotkey with Spark.

Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    // BIKE :: Copy As Plain Text Only

    // To get around a bug in Numbers.app,
    // which seems to stops looking for plain text when
    // it sees HTML and OPML outlines in the clipboard

    ObjC.import("AppKit");

    const main = () => {
        menuItemClicked("Bike")([
            "Edit", "Copy", "Copy"
        ]);

        delay(0.2);

        return fmapLR(
            copyText
        )(
            clipTextLR()
        );
    };

    // ----------------------- JXA -----------------------

    // clipTextLR :: () -> Either String String
    const clipTextLR = () => {
        // Either a message, (if no clip text is found),
        // or the string contents of the clipboard.
        const
            v = ObjC.unwrap(
                $.NSPasteboard.generalPasteboard
                .stringForType($.NSPasteboardTypeString)
            );

        return Boolean(v) && 0 < v.length ? (
            Right(v)
        ) : Left("No utf8-plain-text found in clipboard.");
    };


    // copyText :: String -> IO String
    const copyText = s => {
        const pb = $.NSPasteboard.generalPasteboard;

        return (
            pb.clearContents,
            pb.setStringForType(
                $(s),
                $.NSPasteboardTypeString
            ),
            s
        );
    };

    // menuItemClicked :: String -> [String] -> IO Bool
    const menuItemClicked = strAppName =>
        // Click an OS X app sub-menu item
        // 2nd argument is an array of arbitrary length 
        // (exact menu item labels, giving full path)
        lstMenuPath => {
            const intMenuPath = lstMenuPath.length;

            return intMenuPath > 1 ? (() => {
                const
                    appProcs = Application("System Events")
                    .processes.where({
                        name: strAppName
                    });

                return appProcs.length > 0 ? (
                    Application(strAppName)
                    .activate(),
                    lstMenuPath.slice(1, -1)
                    .reduce(
                        (a, x) => a.menuItems[x].menus[x],
                        appProcs[0].menuBars[0].menus.byName(
                            lstMenuPath[0]
                        )
                    )
                    .menuItems[lstMenuPath[intMenuPath - 1]]
                    .click(),
                    true
                ) : false;
            })() : false;
        };

    // --------------------- GENERIC ---------------------

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


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

    // fmapLR (<$>) :: (b -> c) -> Either a b -> Either a c
    const fmapLR = f =>
        // Either f mapped into the contents of any Right
        // value in e, or e unchanged if is a Left value.
        e => "Left" in e ? (
            e
        ) : Right(f(e.Right));

    return main();
})();
1 Like

Hmm. Sorry, the 1st script works, but the 2nd returns ‘undefined’ for some reason. The 2nd also asked for Accessibility access, which I granted, but not sure if that was intended.

Hmm … I’m having no problems directly pasting from Bike to Numbers. Doesn’t matter if I copied a single row and several rows: in both cases I get test in one cell, or the text into a group of cells. Works as expected for me.

… perhaps there’s something different about my general setup on OS.

Try pasting when the cursor is inside a cell (double clicking that cell)

The return value we can make more communicative :slight_smile:

UPDATE :: (I’ve done that in the copy above)

The question is though, is it successfully copying from Bike in a way that is allowing you to paste into Numbers ?

1 Like

Oops, my bad! I saw ‘undefined’ and thought something went wrong w/o even trying to paste the result. The script works as intended, thank you, Will try to bind it to a hotkey now.

1 Like

I saw ‘undefined’ and thought something went wrong

(It’s returning a more informative value now – modified above : -)

Could be different macOS or Numbers builds, but I think the OP’s experience (and mine experimentally too) is that there’s a problem only when we have double-clicked the clicked the cell and have an editing cursor inside it.

Ah, got it. Makes sense and same for me. I never would have thought to double-clicked to be inside cell. Glad you found a solution!

1 Like

Ok, I set up a shortcut for running the script. thank you again! It’s gonna make my life easier, for sure. Having said that, ‘Edit > Copy as Plain Text only’ built into Bike eventually would be just perfect :wink:

1 Like

Here FWIW, is a slightly fuller version, which may be marginally more solid
(and a little more informative, if it ever finds that it can’t copy something, for some reason)

Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    // BIKE :: Copy As Plain Text Only

    // To get around a bug in Numbers.app,
    // which seems to stop looking for plain text when
    // (inside the editing field of a spreadsheet cell)
    // it sees HTML and OPML outlines in the clipboard.

    // Ver 0.04
    // Rob Trew @2020

    ObjC.import("AppKit");

    // MAIN :: IO ()
    const main = () => {
        const
            bike = Application("Bike"),
            doc = bike.documents.at(0);

        return either(
            alert("Copy as Text Only")
        )(
            clip => clip
        )(
            doc.exists() ? (() => {
                const
                    selectedText = doc.selectedText()
                    .trimStart();

                return Boolean(selectedText) ? (
                    copyText(selectedText),
                    // delay(0.2),
                    bindLR(
                        clipTextLR()
                    )(
                        clip => clip === selectedText ? (
                            Right(clip)
                        ) : Left(
                            [
                                "Text not copied ...",
                                "Try increasing delay."
                            ]
                            .join("\n")
                        )
                    )
                ) : Left("Nothing selected in Bike.");
            })() : Left("No document open in Bike.")
        );
    };

    // ----------------------- JXA -----------------------

    // alert :: String => String -> IO String
    const alert = title =>
        s => {
            const sa = Object.assign(
                Application("System Events"), {
                    includeStandardAdditions: true
                });

            return (
                sa.activate(),
                sa.displayDialog(s, {
                    withTitle: title,
                    buttons: ["OK"],
                    defaultButton: "OK"
                }),
                s
            );
        };


    // clipTextLR :: () -> Either String String
    const clipTextLR = () => {
        // Either a message, (if no clip text is found),
        // or the string contents of the clipboard.
        const
            v = ObjC.unwrap(
                $.NSPasteboard.generalPasteboard
                .stringForType($.NSPasteboardTypeString)
            );

        return Boolean(v) && 0 < v.length ? (
            Right(v)
        ) : Left("No utf8-plain-text found in clipboard.");
    };


    // copyText :: String -> IO String
    const copyText = s => {
        const pb = $.NSPasteboard.generalPasteboard;

        return (
            pb.clearContents,
            pb.setStringForType(
                $(s),
                $.NSPasteboardTypeString
            ),
            s
        );
    };


    // --------------------- GENERIC ---------------------

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


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


    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        // Application of the function fl to the
        // contents of any Left value in e, or
        // the application of fr to its Right value.
        fr => e => "Left" in e ? (
            fl(e.Left)
        ) : fr(e.Right);


    // bindLR (>>=) :: Either a ->
    // (a -> Either b) -> Either b
    const bindLR = lr =>
        // Bind operator for the Either option type.
        // If lr has a Left value then lr unchanged,
        // otherwise the function mf applied to the
        // Right value in lr.
        mf => "Left" in lr ? (
            lr
        ) : mf(lr.Right);

    // sj :: a -> String
    const sj = (...args) =>
        // Abbreviation of showJSON for quick testing.
        // Default indent size is two, which can be
        // overriden by any integer supplied as the
        // first argument of more than one.
        JSON.stringify.apply(
            null,
            1 < args.length && !isNaN(args[0]) ? [
                args[1], null, args[0]
            ] : [args[0], null, 2]
        );

    return sj(main());
})();
1 Like