Skip to main content

Command Palette

Search for a command to run...

Ctrl+S to Save in Oracle APEX

One File, Every Page

Updated
6 min read

Based on APEX Instant Tips episode 199

Users coming from desktop applications have a deeply ingrained habit: press Ctrl+S to save. In Oracle APEX, that shortcut does nothing out of the box — which leads to lost changes and frustration, especially on forms with Interactive Grids where a misplaced keypress can feel costly. The solution is a single JavaScript file you upload once and reference in your Application Attributes. From that moment on, Ctrl+S works on every page in your application — no per-page setup, no conditions, no duplicated code. The script inspects the page at the moment the shortcut is pressed and figures out the right thing to do on its own.

The Problem

APEX doesn't bind Ctrl+S to any save action by default. Users either click the Save button manually...or type Ctrl+S and nothing happens.

One fix would be to add a keyboard handler to each page individually. But that doesn't scale, and it breaks down the moment a page has an Interactive Grid: if the user is mid-edit in a cell, clicking save without first committing that cell edit will silently discard the change.

The script here handles three scenarios automatically, with no page-by-page configuration:

  1. Focus is inside an Interactive Grid — commit the current cell edit first, then trigger the IG's save action

  2. Focus is on a regular edit form — click the visible Save button

  3. Focus is on a create form — click the visible Create button

Set It Up Once

Upload the JavaScript file to your application's static files, then reference it in Application Attributes so it loads on every page.

Step 1 — Upload the file

In Page Designer, go to Shared Components → Static Application Files and upload a file named something like global-shortcuts.js containing the code below.

Step 2 — Reference it in Application Attributes

Go to Shared Components → Application Definition (or click the application name in the builder), open the User Interface tab, and add the file URL to JavaScript → File URLs:

#APP_FILES#global-shortcuts#MIN#.js

That's it. The shortcut is now active on every page without touching individual page definitions.

The Code

apex.jQuery(document).ready(function () {
    self.focus();

    apex.actions.add({
        name: "save",
        label: "Save",
        shortcut: "Ctrl+S",
        action: function (event, focusElement) {
            event.preventDefault();

            // Determine if the focused element is inside an Interactive Grid
            var \(focused = \)(document.activeElement);
            var \(igContainer = \)focused.closest(".a-IG");

            if ($igContainer.length) {
                // Find the parent APEX region to get its static ID
                var \(region = \)igContainer.closest(".js-apex-region");

                if ($region.length) {
                    var regionId = $region.attr("id");
                    var saveBtnIg = $region.find("button[data-action='save']").filter(":visible").first();

                    if (saveBtnIg.length) {
                        try {
                            var ig$ = apex.region(regionId).widget();
                            var grid = ig$.interactiveGrid("getViews", "grid");
                            // Commit current cell value into the model
                            grid.view$.grid("finishEditing");
                            // Use the IG's own save action so it handles
                            // validation and scroll position internally
                            var igActions = apex.region(regionId).call("getActions");
                            igActions.invoke("save");
                            return;
                        } catch (e) {
                            // Region is not a valid IG; fall through to global save
                        }
                    }
                }
            }

            // Not inside an IG or no visible IG save button — try the global save button
            var saveBtn = $("button[data-otel-label='SAVE']").filter(":visible").first();
            if (saveBtn.length) {
                saveBtn.trigger("click");
                return;
            }

            // Fallback: try the create button
            var addBtn = $("button[data-otel-label='CREATE']").filter(":visible").first();
            if (addBtn.length) {
                addBtn.trigger("click");
            }
        }
    });
});

How It Works

Register the shortcut with apex.actions

apex.actions.add({
    name: "save",
    label: "Save",
    shortcut: "Ctrl+S",
    action: function (event, focusElement) { ... }
});

apex.actions is APEX's built-in action framework. By registering the shortcut here rather than binding a raw keydown event, you work with APEX rather than around it. APEX handles cross-platform differences (Ctrl on Windows/Linux, Cmd on Mac) and avoids conflicts with other registered shortcuts.

event.preventDefault() is called immediately inside the action to stop the browser's default "Save Page As" dialog from appearing.

Detect whether focus is inside an Interactive Grid

var \(focused = \)(document.activeElement);
var \(igContainer = \)focused.closest(".a-IG");

document.activeElement gives us the element the user is currently working in. .closest(".a-IG") walks up the DOM looking for the Interactive Grid wrapper. If it finds one, $igContainer.length will be greater than zero and we know we're inside a grid.

Get the region ID

var \(region = \)igContainer.closest(".js-apex-region");
var regionId = $region.attr("id");

Every APEX region gets the CSS class js-apex-region. We walk up from the IG container to find the region wrapper, then grab its id attribute. This is the same ID you set as the Static ID on the region in Page Designer — and it's what apex.region() needs to look up the region's API.

Finish editing the current cell

var ig$ = apex.region(regionId).widget();
var grid = ig$.interactiveGrid("getViews", "grid");
grid.view$.grid("finishEditing");

This is the key step. If the user is mid-edit in a cell, the new value exists in the DOM input but hasn't yet been committed to the IG's data model. Calling finishEditing flushes that value into the model before the save happens — so the change isn't silently discarded.

Invoke the IG's own save action

var igActions = apex.region(regionId).call("getActions");
igActions.invoke("save");

Rather than clicking the save button directly, we go through the IG's internal action context. This means the IG handles its own validation, error display, and scroll-position management the same way it does when the user clicks Save manually.

The whole IG block is wrapped in a try/catch so that if anything goes wrong (e.g. the region ID doesn't resolve to a valid IG), execution falls through to the global save logic below.

Fall back to the page Save / Create button

var saveBtn = $("button[data-otel-label='SAVE']").filter(":visible").first();
if (saveBtn.length) {
    saveBtn.trigger("click");
    return;
}

var addBtn = $("button[data-otel-label='CREATE']").filter(":visible").first();
if (addBtn.length) {
    addBtn.trigger("click");
}

If focus isn't inside an IG, or if the IG path failed, the code looks for the page's standard Save button using its data-otel-label attribute. APEX adds this attribute to built-in buttons, so data-otel-label='SAVE' reliably targets the Save button generated from the Save button template.

The .filter(":visible") call ensures we only interact with buttons that are actually visible on screen — handy on pages where multiple button regions may exist but some are hidden by conditions.

If there's no Save button (i.e. this is a Create/Insert form rather than an Edit form), it falls back to the Create button. This means the same shortcut works on both "create" and "edit" variants of your form pages without any extra configuration.

One Thing to Watch

The self.focus() call at the top ensures the page itself has focus when it loads, which is a prerequisite for keyboard shortcuts to be captured. On pages that auto-focus a field on load (using the Cursor Focus setting), this line is harmless — the field focus takes over immediately after. On pages without auto-focus, it prevents the scenario where shortcuts are silently ignored because the browser's focus is sitting somewhere outside the document.