Watermark Trick Into a Reusable APEX Plug-in

✦
✧
✦
Oracle APEX / Plug-in Diary

I Turned My Watermark Trick Into a Reusable APEX Plug-in

And honestly, it fought me the whole way. Here is the full story, every error I hit, and what finally made it work.

💡 Why I wanted a plug-in

In my last post I made a CONFIDENTIAL watermark using a few lines of CSS. It worked great, but I had to copy that CSS into every page by hand. That felt silly. I wanted something I could just drop onto any region and fill in a few boxes.

So I decided to build it as a Dynamic Action plug-in. The idea is simple. The watermark sits on top of a region that already exists, and a Dynamic Action is perfect for adding behaviour like that on page load. Once it is built I can export it as one file and reuse it anywhere. That part is the dream. Getting there was the hard part.

🧱 Step one, create the plug-in

I went to Shared Components, then Plug-ins, then Create, and picked From Scratch. I set the Name to Watermark and the Type to Dynamic Action. Easy so far.

I tried to click Create right away, but it threw an error saying "Category must have some value".

One issue I faced here is that Category is required and it is easy to miss. I looked at the list and picked Effect, because a watermark is a visual change to an element, which is exactly what Effect is for. After that it saved fine.

🎛️ Step two, the attributes

Next I added five custom attributes. These become the little boxes a developer fills in later, so the plug-in stays flexible without anyone touching code.

No.LabelTypeDefault
1Watermark TextTextCONFIDENTIAL
2Opacity (0 to 1)Text0.10
3Rotation (degrees)Text-25
4Colour (R,G,B)Text31,58,92
5StyleSelect Listsingle

A small tip I learned the hard way. Keep any value that has a comma in it as a Text type. My Colour value is 31,58,92 and that comma matters later in the story.

📜 Step three, the JavaScript file

This file does the real work in the browser. It reads the attributes, builds an overlay div, rotates it, fades it, and drops it on top of the region. I saved it as Watermark.js and uploaded it in the plug-in Files section.

Watermark.js
var atd = window.atd || {};

atd.watermark = (function () {
  "use strict";

  function buildSvg(text, color, opacity, rotate) {
    var svg =
      "<svg xmlns='http://www.w3.org/2000/svg' width='320' height='200'>" +
      "<text x='10' y='110' fill='rgba(" + color + "," + opacity + ")' " +
      "font-size='28' font-family='sans-serif' " +
      "transform='rotate(" + rotate + " 160 100)'>" + text + "</text></svg>";
    return "url(\"data:image/svg+xml;utf8," + svg + "\")";
  }

  function apply(da) {
    da = da || {};
    var a = da.action || {};
    var text    = a.attribute01 || "CONFIDENTIAL";
    var opacity = a.attribute02 || "0.10";
    var rotate  = a.attribute03 || "-25";
    var color   = a.attribute04 || "31,58,92";
    var mode    = a.attribute05 || "single";

    var els = da.affectedElements || apex.jQuery("#emp_report");

    els.each(function () {
      var host = this;

      if (getComputedStyle(host).position === "static") {
        host.style.position = "relative";
      }

      var existing = host.querySelector(":scope > .atd-watermark");
      if (existing) {
        existing.remove();
      }

      var ov = document.createElement("div");
      ov.className = "atd-watermark";
      ov.setAttribute("aria-hidden", "true");
      ov.style.position = "absolute";
      ov.style.inset = "0";
      ov.style.zIndex = "5";
      ov.style.pointerEvents = "none";
      ov.style.userSelect = "none";

      if (mode === "tiled") {
        ov.style.backgroundImage = buildSvg(text, color, opacity, rotate);
        ov.style.backgroundRepeat = "repeat";
      } else {
        ov.style.display = "flex";
        ov.style.alignItems = "center";
        ov.style.justifyContent = "center";
        ov.style.whiteSpace = "nowrap";
        ov.style.fontWeight = "700";
        ov.style.fontSize = "6rem";
        ov.style.letterSpacing = ".4rem";
        ov.style.color = "rgba(" + color + "," + opacity + ")";
        ov.style.transform = "rotate(" + rotate + "deg)";
        ov.textContent = text;
      }

      host.appendChild(ov);
    });
  }

  return { apply: apply };
})();

🗄️ Step four, the PL/SQL render

This is the server side. It loads my JavaScript file and tells APEX which function to run. This tiny block is where most of my pain came from, so look closely.

PL/SQL
procedure render (
    p_dynamic_action in            apex_plugin.t_dynamic_action,
    p_plugin         in            apex_plugin.t_plugin,
    p_param          in            apex_plugin.t_dynamic_action_render_param,
    p_result         in out nocopy apex_plugin.t_dynamic_action_render_result )
is
begin
    apex_javascript.add_library (
        p_name      => 'Watermark',
        p_directory => p_plugin.file_prefix );

    p_result.javascript_function := 'atd.watermark.apply';
end render;

And do not forget the Callbacks tab. You have to type render into the Render Procedure or Function Name box. If you skip this, the plug-in has no entry point at all.

🐛 The errors I fought, one by one

Here is the honest part. Almost nothing worked the first time. I am writing all of these down because if you are building a plug-in, you will probably meet a few of them too.

Error 1. "No render function has been defined for plug-in".

WWV_FLOW_PLUGIN.NO_RENDER_FUNCTION

I had the PL/SQL written, but I forgot to tell APEX its name. I went to the Callbacks tab and typed render into Render Procedure or Function Name. That cleared it.

Error 2. "identifier RENDER must be declared".

PLS-00201: identifier 'RENDER' must be declared

I tried it as a function that returns a result, but it did not work. APEX 26.1 was calling render with four parameters, including p_param and p_result. My signature did not match, so it could not find it. Finally this approach worked. I changed it from a function into a procedure with all four parameters, and I set the result using p_result instead of a return. That is the version in the code above.

Error 3. A 404 on my file, then a MIME type refusal.

Failed to load resource: 404 Watermark.js

One issue I faced that wasted ten minutes. My code asked for watermark in small letters, but my file was Watermark.js with a capital W. The server is case sensitive, so it could not find it. I made the name in the code match the file exactly, capital W, and the file loaded.

Error 4. The one that nearly broke me.

Uncaught SyntaxError: Unexpected token ','

This comma error kept showing up no matter what I changed. I rewrote my JavaScript three times and it did not move even one character. That was the clue. The problem was not my code at all.

I tried changing the JS, the procedure, and the attribute types, and the same comma error stayed. So I stopped editing code and started testing what was actually live.

I disabled the Dynamic Action and reloaded. The errors went away. That told me the Dynamic Action itself was broken. From all my back and forth I had ended up with a messy tree, a stray event node and an oddly named one. A half built event like that writes broken JavaScript onto the page, and that broke the whole dynamic action engine, which is why I also kept seeing apex.da.initDaEventList is not a function.

🎉 What finally worked

Finally this approach worked. I deleted the whole Dynamic Action, including the stray nodes, and built it again from scratch in the right order.

  1. Right click Dynamic Actions and choose Create Dynamic Action.
  2. Set the Event to Page Load first, before touching anything else.
  3. Click the True action and set its Action to Watermark.
  4. Leave the five attributes on their defaults for the first test.
  5. Set Affected Elements, Selection Type Region, and pick the report.
  6. Save and reload with a hard refresh.
The CONFIDENTIAL overlay finally showed up across the report, and sorting and search still worked underneath it. No console errors.

The big lesson for me was that setting the event first is what keeps the Dynamic Action clean. Building it out of order is what created the broken state in the first place.

📌 Things I will remember next time

  1. Pick the Category when you create the plug-in, or it will not save.
  2. Always set the Render Procedure or Function Name in Callbacks.
  3. In 26.1 use a procedure with four parameters, not a function.
  4. File names are case sensitive, so match them exactly.
  5. If the same error survives every code change, the code is not the cause.
  6. Build the Dynamic Action event first, then the action, to avoid a broken tree.

See it in action

This little plug-in took a lot of fighting, but now I can drop a watermark on any page in seconds. Worth it.

Comments

Popular posts from this blog

List Link Attributes and Menu Buttons

Dynamic Triggered Action in Cards

Gemini , Mistral free AI Editoral in Apex 26.1