Ctrl+S to Save in Oracle APEX
One File, Every Page
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:
Focus is inside an Interactive Grid — commit the current cell edit first, then trigger the IG's save action
Focus is on a regular edit form — click the visible Save button
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.
