"use strict";
const IG_STATIC_ID = "emp_ig";
const RESET_MS = 2000;
const SWAP_MS = 120;
/* ===========================
* UI (Icon Animation)
* =========================== */
function animateIcon($btn) {
const $ico = $btn.find(".ig-copy-ico").first();
if (!$ico.length) return;
const prev = $btn.data("igCopyIconTimer");
if (prev) clearTimeout(prev);
$ico.addClass("is-swap");
setTimeout(() => {
$ico.removeClass("fa-copy")
.addClass("fa-check is-success")
.removeClass("is-swap");
}, SWAP_MS);
const timer = setTimeout(() => {
$ico.addClass("is-swap");
setTimeout(() => {
$ico.removeClass("fa-check is-success")
.addClass("fa-copy")
.removeClass("is-swap");
}, SWAP_MS);
}, RESET_MS);
$btn.data("igCopyIconTimer", timer);
}
function showCopyError(msg) {
apex.message.clearErrors();
apex.message.showErrors([{
type: "error",
location: "page",
message: msg,
unsafe: false
}]);
}
/* ===========================
* Clipboard (Focus + Fallback)
* =========================== */
function legacyCopySync(text) {
const ta = document.createElement("textarea");
ta.value = text;
ta.setAttribute("readonly", "");
ta.style.position = "fixed";
ta.style.left = "-9999px";
ta.style.top = "0";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.focus();
ta.select();
let ok = false;
try { ok = document.execCommand("copy"); }
catch (e) { ok = false; }
finally { document.body.removeChild(ta); }
return ok;
}
function copyFromClick(text) {
const t = (text ?? "").toString();
try { window.focus(); } catch (e) {}
try { document.body?.focus?.(); } catch (e) {}
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(t)
.then(() => true)
.catch(() => legacyCopySync(t));
}
return Promise.resolve(legacyCopySync(t));
}
/* ===========================
* Value Normalization (LOV + NULL)
* - If null/undefined/"" => returns "EMPTY"
* - If object (LOV) => tries display (d/display/label/text) then return (v)
* =========================== */
function getDisplayOrValue(v) {
if (v === null || v === undefined || v === "") return "EMPTY";
if (Array.isArray(v)) {
const parts = v.map(getDisplayOrValue);
return parts.join(", ");
}
if (typeof v === "object") {
// Common formats for LOV/complex values
if (v.d !== undefined) return getDisplayOrValue(v.d);
if (v.display !== undefined) return getDisplayOrValue(v.display);
if (v.label !== undefined) return getDisplayOrValue(v.label);
if (v.text !== undefined) return getDisplayOrValue(v.text);
// Fallback: return value
if (v.v !== undefined) return getDisplayOrValue(v.v);
// Final fallback: avoid [object Object]
try { return JSON.stringify(v); } catch (e) { return "EMPTY"; }
}
return String(v);
}
/* ===========================
* IG Helpers
* =========================== */
function getGrid() {
const ig$ = apex.region(IG_STATIC_ID).widget();
return ig$.interactiveGrid("getViews", "grid");
}
function getRecordFromButton(grid, $btn) {
const model = grid.model;
// Preferred: TR[data-id]
const rid = $btn.closest("tr[data-id]").attr("data-id");
if (rid) {
const rec = model.getRecord(rid);
if (rec) return rec;
}
// Fallback: Context Record
const ctx = grid.getContextRecord($btn);
if (typeof ctx === "string") {
const rec2 = model.getRecord(ctx);
if (rec2) return rec2;
}
if (ctx) return ctx;
return null;
}
function getValueByField(model, rec, fieldName) {
const fields = model.getOption("fields") || {};
const keys = Object.keys(fields);
const fn = keys.find(k => k.toUpperCase() === String(fieldName).toUpperCase());
if (!fn) return { ok: false, reason: "field_not_found" };
// 1) Preferred: getValue(fieldKey)
if (typeof model.getFieldKey === "function" && typeof model.getValue === "function") {
try {
const fk = model.getFieldKey(fn);
const v = model.getValue(rec, fk);
if (v !== undefined) return { ok: true, value: v, field: fn };
} catch (e) {}
}
// 2) Fallback: Array + Index
const idx = fields[fn] && typeof fields[fn].index === "number" ? fields[fn].index : null;
if (idx !== null && Array.isArray(rec)) {
return { ok: true, value: rec[idx], field: fn };
}
// 3) Final Fallback: Direct name (if supported)
if (typeof model.getValue === "function") {
try {
return { ok: true, value: model.getValue(rec, fn), field: fn };
} catch (e) {}
}
return { ok: false, reason: "no_strategy_worked", field: fn };
}
/* ===========================
* Visual Order (Reorder-safe)
* - Maps Header C... -> fieldName via fieldsMap[field].id
* =========================== */
function getVisibleFieldsInHeaderOrder(grid, $btn) {
const model = grid.model;
const fieldsMap = model.getOption("fields") || {};
// colId (C...) -> fieldName
const colIdToField = {};
Object.keys(fieldsMap).forEach(fn => {
const meta = fieldsMap[fn];
if (meta && meta.id) {
colIdToField[String(meta.id)] = fn;
}
});
const $ig = $btn.closest(".a-IG");
// Header TH elements
const $ths = $ig.find("th.a-GV-header").filter(function () {
const $th = $(this);
if ($th.is(":hidden")) return false;
// Must have a header label ID ending in _HDR
const $lbl = $th.find(".a-GV-headerLabel[id$='_HDR']");
return $lbl.length > 0;
});
const fieldsInOrder = [];
$ths.each(function () {
const $th = $(this);
const labelId = $th.find(".a-GV-headerLabel[id$='_HDR']").attr("id"); // C..._HDR
if (!labelId) return;
const colId = labelId.replace(/_HDR$/, ""); // C...
const fieldName = colIdToField[colId];
if (!fieldName) return;
// Filter technical columns or buttons
if (["APEX$ROW_ACTION", "ROWID", "COPY_COL", "COPY_ROW", "_meta"].includes(fieldName)) return;
fieldsInOrder.push(fieldName);
});
// Fallback if DOM scanning fails
if (!fieldsInOrder.length) {
return Object.keys(fieldsMap).filter(f =>
!["APEX$ROW_ACTION", "ROWID", "COPY_COL", "COPY_ROW", "_meta"].includes(f)
);
}
return fieldsInOrder;
}
function escapeForSemicolonCSV(v) {
// Normalizes LOV and NULL to "EMPTY"
let s = getDisplayOrValue(v);
// Excel/CSV with ; (escapes when necessary)
if (/[;"\r\n]/.test(s)) s = `"${s.replace(/"/g, '""')}"`;
return s;
}
/* ===========================
* Actions
* =========================== */
function copyCell($btn) {
const field = String($btn.data("copy-col") || "").trim();
const grid = getGrid();
const model = grid.model;
const rec = getRecordFromButton(grid, $btn);
if (!rec || !field) {
showCopyError("It was not possible to identify the row/field.");
return;
}
const r = getValueByField(model, rec, field);
if (!r.ok) {
showCopyError(`Could not read field "${field}" from model. Reason: ${r.reason || "unknown"}`);
return;
}
// Converts LOV object to display text and null to "EMPTY"
copyFromClick(getDisplayOrValue(r.value)).then(ok => {
if (ok) animateIcon($btn);
else showCopyError("Failed to copy to clipboard.");
});
}
function copyRowAsSemicolon($btn) {
const grid = getGrid();
const model = grid.model;
const rec = getRecordFromButton(grid, $btn);
if (!rec) {
showCopyError("It was not possible to identify the row (record).");
return;
}
// User's VISUAL ORDER (reorder-safe)
const fieldNames = getVisibleFieldsInHeaderOrder(grid, $btn);
const values = fieldNames.map(f => {
const rr = getValueByField(model, rec, f);
// rr.value can be an object (LOV) or null -> normalization
return escapeForSemicolonCSV(rr.ok ? rr.value : null);
});
copyFromClick(values.join(";")).then(ok => {
if (ok) animateIcon($btn);
else showCopyError("Failed to copy to clipboard.");
});
}
/* ===========================
* Handlers (NO inline onclick)
* =========================== */
const SEL_CELL = `#${IG_STATIC_ID} .js-ig-copy-cell`;
const SEL_ROW = `#${IG_STATIC_ID} .js-ig-copy-row`;
// Prevents duplicate handlers on refresh/reload
$(document).off("click.igCopyCell", SEL_CELL);
$(document).off("click.igCopyRow", SEL_ROW);
$(document).on("click.igCopyCell", SEL_CELL, function (e) {
e.preventDefault();
e.stopPropagation();
copyCell($(this));
});
$(document).on("click.igCopyRow", SEL_ROW, function (e) {
e.preventDefault();
e.stopPropagation();
copyRowAsSemicolon($(this));
});
})(apex.jQuery);
Step 4: Add CSS Class to Target Column
CSS Code :
/* smooth transition */
#emp_ig .ig-copy-ico{
display: inline-block;
transition: transform .14s ease, opacity .14s ease, color .14s ease;
transform: scale(1);
opacity: 1;
}
/* vanish effect */
#emp_ig .ig-copy-ico.is-swap {
transform: scale(0.70);
opacity: 0;
}
/* success */
#emp_ig .ig-copy-ico.is-success {
color: #22c55e;
transform: scale(1.10);
opacity: 1;
}
Step 5: Clipboard Copy Logic
Now your IG is ready in frontend for columns and rows copy purpose.
Use JavaScript to copy values dynamically when user clicks the cell.
This ensures:
✔ No page refresh
✔ Instant copy
✔ Smooth UX
Comments
Post a Comment