logo

Are you need IT Support Engineer? Free Consultant

SAC AutoSum Custom Widget — Excel’s Status Bar, Na…

  • By sujay
  • 25/04/2026
  • 10 Views

The SAC AutoSum Custom Widget consists of just two files — widget.json and widget.js. No external libraries, no dependencies, no backend.

The widget.json defines the widget's identity, its configurable properties (colors, locale, which tiles to show), and two methods: clearValues() and addValue(). These are the bridge to the Analytical Application.

The widget.js contains the full logic: value parsing, aggregate calculation, and rendering — all encapsulated in a Web Component with Shadow DOM.

The integration on the SAC side is a single script block in the Table Widget's onSelect event: clear the previous selection, then loop through the selected cells and pass each raw value to the widget via addValue(). The widget handles the rest — live, with every selection change.

One bonus feature: select exactly two cells, and the widget automatically shows a Delta tile — absolute difference and percentage deviation. No extra step, no configuration needed.

Code for: widgest.json

{
  "id": "comdirksacautosum",
  "version": "1.0.4",
  "name": "SAC AutoSum",
  "description": "Zeigt SUM, AVG, MIN, MAX und COUNT der selektierten Tabellenzellen an — analog zur Excel-Statusleiste.",
  "vendor": "SAP",
  "eula": "",
  "license": "",
  "newInstancePrefix": "AutoSumWidget",
  "webcomponents": [
    {
      "kind": "main",
      "tag": "com-dirk-sac-autosum",
      "url": "/widget.js",
      "integrity": "",
      "ignoreIntegrity": true
    }
  ],
  "properties": {
    "locale": {
      "type": "string",
      "description": "Zahlenformat-Locale (z.B. de-DE, en-US)",
      "default": "de-DE"
    },
    "showSum": {
      "type": "boolean",
      "description": "Summe anzeigen",
      "default": true
    },
    "showAvg": {
      "type": "boolean",
      "description": "Durchschnitt anzeigen",
      "default": true
    },
    "showMin": {
      "type": "boolean",
      "description": "Minimum anzeigen",
      "default": true
    },
    "showMax": {
      "type": "boolean",
      "description": "Maximum anzeigen",
      "default": true
    },
    "showCount": {
      "type": "boolean",
      "description": "Anzahl anzeigen",
      "default": true
    },
    "showDelta": {
      "type": "boolean",
      "description": "Show difference tile when exactly 2 cells are selected",
      "default": true
    },
    "accentColor": {
      "type": "string",
      "description": "Akzentfarbe (Trennlinien), z.B. #0070F2",
      "default": "#0070F2"
    },
    "labelColor": {
      "type": "string",
      "description": "Farbe der Beschriftungen (SUM, AVG, ...), z.B. #6A6D70",
      "default": "#6A6D70"
    },
    "valueColor": {
      "type": "string",
      "description": "Farbe der Zahlenwerte, z.B. #32363A",
      "default": "#32363A"
    },
    "backgroundColor": {
      "type": "string",
      "description": "Hintergrundfarbe des Widgets, z.B. #FFFFFF",
      "default": "#F5F6F7"
    }
  },
  "methods": {
    "addValue": {
      "description": "Fuegt einen einzelnen numerischen Wert zur Selektion hinzu. Pro Zelle einmal aufrufen.",
      "parameters": [
        {
          "name": "value",
          "type": "string",
          "description": "Numerischer Wert (rawValue aus getResultSet)"
        }
      ]
    },
    "clearValues": {
      "description": "Setzt die Selektion zurueck. Vor jeder neuen Selektion aufrufen."
    }
  },
  "events": {
    "onSelectionChange": {
      "description": "Feuert wenn sich die Selektion aendert"
    }
  }
}

 

code for widget.js

(function () {
    'use strict';

    // ── Defaults ────────────────────────────────────────────────────────────

    function defaultProps() {
        return {
            selectedValues: '[]',
            locale: 'de-DE',
            showSum: 'true',
            showAvg: 'true',
            showMin: 'true',
            showMax: 'true',
            showCount: 'true',
            showDelta: 'true',
            accentColor: '#0070F2',
            labelColor: '#6A6D70',
            valueColor: '#32363A',
            backgroundColor: '#F5F6F7'
        };
    }

    // ── Helpers ─────────────────────────────────────────────────────────────

    function parseValues(jsonStr) {
        if (!jsonStr || jsonStr === '[]') return [];
        try {
            var arr = JSON.parse(jsonStr);
            var result = [];
            for (var i = 0; i < arr.length; i++) {
                var n;
                if (typeof arr[i] === 'number') {
                    n = arr[i];
                } else {
                    var s = String(arr[i]).trim();
                    var lastDot = s.lastIndexOf('.');
                    var lastComma = s.lastIndexOf(',');
                    if (lastComma > lastDot) {
                        s = s.replace(/\./g, '').replace(',', '.');
                    } else {
                        s = s.replace(/,/g, '');
                    }
                    n = parseFloat(s);
                }
                if (!isNaN(n)) result.push(n);
            }
            return result;
        } catch (e) {
            return [];
        }
    }

    function computeStats(vals) {
        if (!vals || vals.length === 0) return null;
        var sum = 0;
        var min = vals[0];
        var max = vals[0];
        for (var i = 0; i < vals.length; i++) {
            sum += vals[i];
            if (vals[i] < min) min = vals[i];
            if (vals[i] > max) max = vals[i];
        }
        return { sum: sum, avg: sum / vals.length, min: min, max: max, count: vals.length };
    }

    function computeDelta(vals) {
        if (!vals || vals.length !== 2) return null;
        var diff = vals[0] - vals[1];
        var pct = vals[1] !== 0 ? (diff / Math.abs(vals[1])) * 100 : null;
        return { diff: diff, pct: pct };
    }

    function formatNum(n, locale) {
        try {
            return n.toLocaleString(locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
        } catch (e) {
            return String(Math.round(n * 100) / 100);
        }
    }

    function buildCSS(props) {
        return '';
    }

    function buildTiles(stats, props, delta) {
        var locale = props.locale || 'de-DE';
        var idle = !stats;
        var tiles = [
            { show: props.showSum,   label: 'Sum',   val: idle ? '—' : formatNum(stats.sum, locale),   pct: null },
            { show: props.showAvg,   label: 'Avg',   val: idle ? '—' : formatNum(stats.avg, locale),   pct: null },
            { show: props.showMin,   label: 'Min',   val: idle ? '—' : formatNum(stats.min, locale),   pct: null },
            { show: props.showMax,   label: 'Max',   val: idle ? '—' : formatNum(stats.max, locale),   pct: null },
            { show: props.showCount, label: 'Count', val: idle ? '—' : String(stats.count),            pct: null }
        ];

        var html="
"; for (var i = 0; i < tiles.length; i++) { var visible = tiles[i].show === true || tiles[i].show === 'true'; if (!visible) continue; html += '

' + '' + tiles[i].label + '' + '' + tiles[i].val + '' + '

'; } // Delta tile — only when exactly 2 values selected and showDelta enabled var showDelta = props.showDelta === true || props.showDelta === 'true'; if (showDelta && delta !== null) { var diffStr = formatNum(delta.diff, locale); var pctStr = delta.pct !== null ? formatNum(delta.pct, locale) + ' %' : 'n/a'; html += '

' + 'Delta' + '' + diffStr + '' + '' + pctStr + '' + '

'; } html += '
'; html += '

Copied!

'; return html; } // ── Runtime Widget ─────────────────────────────────────────────────────── class AutoSumWidget extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this._props = null; this._values = []; } connectedCallback() { if (!this._props) { this._props = defaultProps(); } this._render(); } onCustomWidgetAfterUpdate(changedProperties) { if (!this._props) { this._props = defaultProps(); } for (var key in changedProperties) { if (Object.prototype.hasOwnProperty.call(changedProperties, key)) { this._props[key] = changedProperties[key]; } } this._render(); } onCustomWidgetBeforeUpdate(changedProperties) { } addValue(value) { var n = parseFloat(String(value)); if (!isNaN(n)) { this._values.push(n); } this._render(); } clearValues() { this._values = []; this._render(); } _render() { var props = this._props || defaultProps(); var stats = computeStats(this._values); var delta = computeDelta(this._values); this.shadowRoot.innerHTML = buildCSS(props) + buildTiles(stats, props, delta); this._attachCopyHandlers(); } _attachCopyHandlers() { var self = this; var bar = this.shadowRoot.querySelector('.bar'); if (!bar) return; bar.addEventListener('click', function (e) { var tile = e.target.closest('.tile'); if (!tile) return; var valueEl = tile.querySelector('.tile-value'); if (!valueEl) return; var text = valueEl.textContent.trim(); if (text === '—') return; try { navigator.clipboard.writeText(text).then(function () { self._showToast(); }); } catch (err) { var ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); self._showToast(); } }); } _showToast() { var toast = this.shadowRoot.querySelector('.toast'); if (!toast) return; toast.classList.add('show'); setTimeout(function () { toast.classList.remove('show'); }, 1500); } } // ── Builder Widget ─────────────────────────────────────────────────────── class AutoSumWidgetBuilder extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this._props = null; } connectedCallback() { if (!this._props) { this._props = defaultProps(); } this._render(); } onCustomWidgetAfterUpdate(changedProperties) { if (!this._props) { this._props = defaultProps(); } for (var key in changedProperties) { if (Object.prototype.hasOwnProperty.call(changedProperties, key)) { this._props[key] = changedProperties[key]; } } this._render(); } onCustomWidgetBeforeUpdate(changedProperties) { } _render() { var props = this._props || defaultProps(); var exampleStats = { sum: 42000, avg: 8400, min: 2.46, max: 24.72, count: 5 }; var html = buildCSS(props) + buildTiles(exampleStats, props, null) + '

AAp-Script: AutoSumWidget_1.clearValues() + addValue()

'; this.shadowRoot.innerHTML = html; } } // ── Register ───────────────────────────────────────────────────────────── customElements.define('com-dirk-sac-autosum', AutoSumWidget); customElements.define('com-dirk-sac-autosum-builder', AutoSumWidgetBuilder); }());

 onSelect Function:

// SAC AAp Script: Table_1.onSelect
// Einfügen im SAC Script-Editor unter Table_1 → onSelect
//
// Überträgt selektierte Tabellenzellen in den Business Calculator (BusinessCalc_1).
// Funktioniert mit Einzel- und Mehrfachselektion.
//
// Voraussetzung: Widget heißt BusinessCalc_1, Table heißt Table_1
// Den Key "Account" ggf. durch den modellspezifischen Dimensions-Key ersetzen.
// Tipp: Key ermitteln mit:
//   var r = Table_1.getDataSource().getResultSet(Table_1.getSelections()[0]);
//   for (var k in r[0]) { Text_1.applyText(k); }

Table_1.onSelect = function() {
    var sel = Table_1.getSelections();
    if (sel.length === 0) { return; }
    for (var i = 0; i < sel.length; i++) {
        var results = Table_1.getDataSource().getResultSet(sel[i]);
        if (results.length > 0) {
            var cell = results[0]["Account"];
            if (cell) {
                BusinessCalc_1.addToTape(cell.rawValue, cell.description);
            }
        }
    }
};


// ---------------------------------------------------------------------------
// ALTERNATIVE: Generischer Key "@MeasureDimension" (modelunabhängig)
// Funktioniert bei Standard-SAC-Modellen. Falls "Account" nicht passt,
// diesen Block stattdessen verwenden:
// ---------------------------------------------------------------------------
//
// Table_1.onSelect = function() {
//     var sel = Table_1.getSelections();
//     if (sel.length === 0) { return; }
//     for (var i = 0; i < sel.length; i++) {
//         var results = Table_1.getDataSource().getResultSet(sel[i]);
//         if (results.length > 0) {
//             var cell = results[0]["@MeasureDimension"];
//             if (cell) {
//                 BusinessCalc_1.addToTape(cell.rawValue, cell.description);
//             }
//         }
//     }
// };

One thing worth noting for the AAp-Script integration: the getResultSet() call requires a dimension key to read the cell value. In the example script, this key is "Account" — but this is model-specific and may differ in your data model.

If you're unsure which key to use, the script includes a small helper snippet: run it once, and it prints all available keys for the selected cell. As a model-independent alternative, "@MeasureDimension" works in most standard SAC models.

The Deployment:

To deploy the widget in SAC, create a ZIP file containing both widget.json and widget.js — both files must sit in the root of the ZIP, not in a subfolder. Then upload into SAC and have fun.

Let me know what you made out of it. Like to hear about enhancements and such.

 

Source link

Leave a Reply

Your email address will not be published. Required fields are marked *

//
Our customer support team is here to answer your questions. Ask us anything!
👋 Hi, how can I help?