NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Business Central - BLISS
// @namespace http://tampermonkey.net/
// @version 0.12.1
// @description Basic Line ID for Swift Scanning
// @author SNAD
// @match https://businesscentral.dynamics.com/*
// @match https://*.businesscentral.dynamics.com/*
// @updateURL https://openuserjs.org/meta/SNAD/Business_Central_-_BLISS_-_Basic_Line_ID_for_Swift_Scanning.meta.js
// @downloadURL https://openuserjs.org/install/SNAD/Business_Central_-_BLISS_-_Basic_Line_ID_for_Swift_Scanning.user.js
// @copyright 2026, SNAD (https://openuserjs.org/users/SNAD)
// @license GPL-3.0-or-later
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const ATTENTION_SELECTOR = '.ms-nav-attention-styleview, [class*="style-attention"]';
// v0.11 painted every selected row by walking its entire subtree in JS and writing
// several `!important` inline styles onto EVERY descendant element, every single time
// the mutation observer fired. On heavy, highly reactive pages (Hardware Configurator)
// that observer can fire very often, and each firing did O(subtree size) work with no
// debouncing - the cost of our own re-paint compounded with however busy the page
// already was. This version does the exact same visual result with a single injected
// stylesheet: JS only toggles a class + data attribute on the row (O(1) per row), and
// lets the browser's CSS engine cascade the color down instead of walking the DOM by
// hand. The gradient-background + color-scheme hint (needed so Chrome's own auto-dark
// repaint doesn't re-invert colors we already picked) and the two hand-tuned palettes
// are unchanged from v0.11 - only how they get applied changed.
const STYLE_ID = 'bliss-style';
if (!document.getElementById(STYLE_ID)) {
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = `
:root {
--bliss-normal-bg: #b2e9ec;
--bliss-red-bg: #ecb2c0;
--bliss-normal-text: #000000;
}
@media (prefers-color-scheme: dark) {
:root {
--bliss-normal-bg: #0f3d42;
--bliss-red-bg: #42121d;
--bliss-normal-text: #e8e8e8;
}
}
tr.bliss-row[data-bliss] { box-shadow: none !important; }
tr.bliss-row[data-bliss="normal"] { --bliss-bg: var(--bliss-normal-bg); }
tr.bliss-row[data-bliss="red"] { --bliss-bg: var(--bliss-red-bg); }
tr.bliss-row[data-bliss],
tr.bliss-row[data-bliss] td:not(.grid-selection-column),
tr.bliss-row[data-bliss] td:not(.grid-selection-column) * {
background-image: linear-gradient(var(--bliss-bg), var(--bliss-bg)) !important;
background-color: var(--bliss-bg) !important;
color-scheme: light !important;
}
tr.bliss-row[data-bliss] td.grid-selection-column {
background: transparent !important;
background-image: none !important;
}
/* One consistent text color across the row, except genuinely-flagged attention
elements (and their descendants), which keep BC's native color. Each :not()
clause must stay on the same line as the selector it modifies - CSS treats
any whitespace between compound selectors as a descendant combinator. */
tr.bliss-row[data-bliss],
tr.bliss-row[data-bliss] td:not(.grid-selection-column),
tr.bliss-row[data-bliss] td:not(.grid-selection-column) *:not(.ms-nav-attention-styleview):not([class*="style-attention"]):not(.ms-nav-attention-styleview *):not([class*="style-attention"] *) {
color: var(--bliss-normal-text) !important;
}
`;
(document.head || document.documentElement).appendChild(style);
}
function isAttentionRow(row) {
return row.querySelector(ATTENTION_SELECTOR) !== null;
}
let lastSelectedRows = new Set();
function forceBlueHighlights() {
const currentlySelected = new Set();
document.querySelectorAll('tbody tr[role="row"][aria-selected="true"]').forEach(row => {
const rowKey = row.getAttribute('rowkey') || row.getAttribute('aria-rowindex') || 'unknown';
currentlySelected.add(rowKey);
row.classList.add('bliss-row');
row.dataset.bliss = isAttentionRow(row) ? 'red' : 'normal';
});
// Cleanup deselected rows
lastSelectedRows.forEach(oldKey => {
if (!currentlySelected.has(oldKey)) {
document.querySelectorAll('tr.bliss-row').forEach(row => {
const rowKey = row.getAttribute('rowkey') || row.getAttribute('aria-rowindex');
if (rowKey === oldKey) {
row.classList.remove('bliss-row');
delete row.dataset.bliss;
}
});
}
});
lastSelectedRows = currentlySelected;
}
// Coalesce bursts of mutations into at most one re-render per animation frame, so a
// page that's already busy (lots of mutations firing close together) doesn't also get
// hit with a pile of synchronous re-renders stacking up on top of that.
let rafScheduled = false;
function scheduleForceHighlights() {
if (rafScheduled) return;
rafScheduled = true;
requestAnimationFrame(() => {
rafScheduled = false;
forceBlueHighlights();
});
}
const observer = new MutationObserver((mutations) => {
if (mutations.some(m => m.attributeName === 'aria-selected' || m.addedNodes.length)) {
scheduleForceHighlights();
}
});
function init() {
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['aria-selected', 'class']
});
forceBlueHighlights();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
window.addEventListener('popstate', forceBlueHighlights);
const originalPush = history.pushState;
history.pushState = function (...args) {
originalPush.apply(this, args);
setTimeout(forceBlueHighlights, 200);
};
console.log('BLISS ACTIVE: Selected lines now highlighted');
})();