(function () { /* ------------------------------------ */ /* Basic Form Metadata Enhancements */ /* ------------------------------------ */ /* -- Ghost Text - Add text to the fields which can serve as example text overwritten by end users. -- */ class GhostText extends HTMLSpanElement { constructor() { super(); } } customElements.define("ghost-text", GhostText, { extends: "span" }); let ghosts = document.querySelectorAll('.ghost-text, ghost-text'); ghosts.forEach((g) => { try { g.closest('.cell').querySelector('textarea, input').placeholder = g.innerText; } catch (error) { console.error('GhostText Insert Failed', error.message); } }); class SectionBody extends HTMLDivElement { constructor() { super(); } } customElements.define("section-body", SectionBody, { extends: "div" }); let secBodies = document.querySelectorAll('section-body'); secBodies.forEach((b) => { try { let fs = b.closest('fieldset'); let title = b.closest('.section-title'); title.insertAdjacentElement('afterend', b); } catch (error) { } }); /* -- Dialogs Description Override - Basic Forms/Multistep forms allow changing the title but not the description natively. This allows a similar technique to ghost-text for overriding the description (detects "" entered into where the dialog title is overriden to also override the description) -- */ $('.modal[role="dialog"]').on('shown.bs.modal', function () { const modal = this; const dialog = modal.querySelector('.modal-dialog'); if (!dialog) return; const title = dialog.querySelector('.modal-title'); const body = dialog.querySelector('.modal-body'); if (!title || !body) return; const originalDescription = title.querySelector('description'); if (originalDescription) { // Remove existing description in body if any const existing = body.querySelector('description'); if (existing) existing.remove(); // Clone and append const cloneDesc = originalDescription.cloneNode(true); body.innerHTML = ""; body.appendChild(cloneDesc); title.setAttribute('aria-label', title.innerText.trim()); } }); /* ----------------------------- */ /* General UI/UX Cleanup */ /* ----------------------------- */ /* -- Remove Empty Rows (e.g. clearfix, spacing artifacts) -- */ let rows = document.querySelectorAll(".crmEntityFormView table.section tr:not(:has(.table-info))"); rows.forEach((row) => { row.remove(); }); /* -- Equalize Label Heights & Padding -- */ var formTables = document.querySelectorAll('fieldset > table'); formTables.forEach((table) => { table.querySelectorAll('tr').forEach((row) => { const labels = row.querySelectorAll('td.cell .table-info label'); if (!labels.length) return; // Ensure block layout and remove any previous min-height labels.forEach((label) => { label.style.display = 'block'; label.style.minHeight = ''; // reset before measuring }); // Measure after layout updates requestAnimationFrame(() => { const maxHeight = Array.from(labels).reduce((max, label) => { const h = label.getBoundingClientRect().height; return Math.max(max, h); }, 0); if (maxHeight > 0) { labels.forEach((label) => { label.style.minHeight = `${maxHeight}px`; }); } }); }); }); /* -- Force External links to open in new tabs. --*/ const LOCAL_WEB_ROOTS = ["powerappsportals.com","international.canada.ca"]; const SR_EXTERNAL_MESSAGE = { "en": `(opens in a new tab, external link)`, "fr": `(s'ouvre dans un nouvel onglet, lien externe)` } document.querySelectorAll('body a[href]').forEach((link) => { if (!LOCAL_WEB_ROOTS.some(substring => link.href.includes(substring))) { link.setAttribute('target', '_blank'); link.insertAdjacentHTML('beforeend',SR_EXTERNAL_MESSAGE[LANG]); } }); /* ----------------------------- */ /* Control-Specific Overrides */ /* ----------------------------- */ try { /* --- TextAreas --- */ /* Resize textareas to display the full contents while typing */ function resizeTA(el) { el.style.height = ""; el.style.height = el.scrollHeight + 5 + "px"; } const textAreas = document.querySelectorAll("textarea"); textAreas.forEach((ta) => { ta.addEventListener("input", (t) => { resizeTA(t.target); }); resizeTA(ta); }); } catch {} try { /* --- Integers - Restrict iput to numbers only --- */ document.querySelectorAll('.cell.integer input.integer').forEach((i)=>{ i.setAttribute('type', 'number'); }); } catch {} try { /* ---- QuickView Handling ---- */ document.querySelectorAll('.crmquickform-cell').forEach((QVCell) => { const QV_IFRAME = QVCell.querySelector('iframe'); const IFRAME_OBSERVER_OPTIONS = { attributes: true, childList: false, subtree: false } const SPINNER_MARKUP = ``; QVCell.querySelector('.control').insertAdjacentHTML('afterbegin', SPINNER_MARKUP); const QV_IFRAME_OBSERVER = new MutationObserver((mutations) => { if (QV_IFRAME.style.height != '') { QV_IFRAME_OBSERVER.disconnect(); QV_IFRAME.classList.add('loaded'); QVCell.classList.add('QV-loaded'); } }); QV_IFRAME_OBSERVER.observe(QV_IFRAME, IFRAME_OBSERVER_OPTIONS); }) } catch {} try { /* --- Filetype Whitelist Note this can be used to edit the files accepted through the portal at large (for convenience, not security) */ const ALLOWED_FILE_EXTENSIONS = [".docx", ".pptx", ".xlsx", ".rtf"]; const FILE_ALERT_STR = { 'en': 'Filetype not allowed', 'fr': 'Type de fichier non autorisé' } const ALLOWED_TYPES = [ "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "text/plain", "application/pdf" ]; fileInputTypeCheckListener = function (e) { const file = this.files.item(0); if (file && !ALLOWED_TYPES.includes(file.type)) { let warning = FILE_ALERT_STR[LANG] || FILE_ALERT_STR['en']; alert(warning); let ctx = window; const rootID = this.id.replace("_input_file", ""); const deleteButton = document.querySelector("#" + rootID + '_delete_button'); ctx.deleteFile(deleteButton, rootID); } }; let fileInputs = document.querySelectorAll('input[type="file"]'); fileInputs.forEach((input) => { input.setAttribute("accept", ALLOWED_TYPES.join(',')); input.addEventListener('change', fileInputTypeCheckListener); }) } catch {} try { /* -- Horizonatal Radio Controls -- Grouping input/label to ensure line breaks in correct spot as CSS alone can't accomplish */ document.querySelectorAll(".picklist.horizontal") .forEach((picklist) => { let frag = document.createDocumentFragment(); let inputs = Array.from(picklist.children).filter(el => el.tagName === 'INPUT'); inputs.forEach(input => { let group = document.createElement('div'); let label = input.labels[0]; group.classList.add('group'); group.appendChild(input); group.appendChild(label); frag.appendChild(group); }); picklist.appendChild(frag); }); /* Sort selects alphabetically with "other" as last option */ document.querySelectorAll('select.sort-alphabetical').forEach(el => WebHelpFunc.sortList(el)); /* -- Convert Selects to Radios */ function convertSelectToRadio(select, layout = 'horizontal') { const cell = select.closest('.cell'), controlDiv = select.closest('.control'), infoLabel = cell.querySelector('.info label'); cell.setAttribute('role', 'radiogroup'); const container = document.createElement('div'); container.classList.add('picklist', layout); var listisEmpty = (select.length == 1) ? (select.children[0].value == '' || select.children[0].value == null) : false; if (listisEmpty) { console.log(select.id + ' is empty no options to convert to radio buttons, skipping'); return; } Array.from(select.options).forEach((option, i) => { if (option.value == '') return; const div = document.createElement('div'), input = document.createElement('input'), label = document.createElement('label'); div.className = 'group'; input.type = 'radio'; input.name = select.id; input.id = `${select.id}_${i}`; input.value = option.value; if (option.selected) input.checked = true; label.htmlFor = input.id; const labelText = option.value ? option.text : 'unnseleted'; label.innerHTML = `${infoLabel.innerText}${labelText}`; div.appendChild(input); div.appendChild(label); container.appendChild(div); input.addEventListener('change', () => { if (input.checked) { select.value = input.value; select.dispatchEvent(new Event('change')); } }); }); WebHelpFunc.hideEl(select); controlDiv.appendChild(container); select.addEventListener('change', () => { const radio = container.querySelector(`input[type="radio"][value="${select.value}"]`); if (radio) radio.checked = true; }); } document.querySelectorAll('select.render-as-radio-list').forEach(el => convertSelectToRadio(el)); } catch {} try { /* --- RTF's --- */ /* (GCDS load css on portal only by adding to iframe outside in) */ RTFS = document.querySelectorAll('.form-control-cell > .control [id^="PcfControlConfig"][data-pcf-control*="MscrmControls.RichTextEditor.RichTextEditorControl"] + [id$="ControlView"]') // Create a new MutationObserver instance const rtfCountObserver = new MutationObserver((mutationsList) => { mutationsList.forEach((mutation) => { if (mutation.type === 'attributes' && mutation.attributeName == 'charcount') { const charCounterDisplay = mutation.target.querySelector('.maxcharcounter'); const maxChar = mutation.target.getAttribute("maxChar"); let count = mutation.target.getAttribute("charcount"); charCounterDisplay.innerHTML = count + ' / ' + maxChar; } }); }); RTFS.forEach((el) => { WebHelpFunc.waitForElement('.fullPageContentEditorFrame', el) .then((el) => { el.contentDocument.body.insertAdjacentHTML('beforeend', ''); el.closest('.editorContainer').style.marginInlineStart = "calc(-2* var(--gcds-input-border-width))!important"; el.closest('.editorContainer').style.width = "calc(100% + 4*var(--gcds-input-border-width))!important"; }); const BUFFER_RATIO = 5; const cell = el.closest(".cell"); const maxValidator = cell.querySelector("[id^=MaximumLengthValidator]"); const numbersInMessage = maxValidator.errormessage.match(/\d+/g); const maxLength = numbersInMessage ? parseInt(numbersInMessage[numbersInMessage.length - 1], 10) / BUFFER_RATIO : null; const inputCounterDisplay = document.createElement('span'); inputCounterDisplay.classList.add('maxcharcounter', 'rtf-counter'); inputCounterDisplay.setAttribute('aria-live', "polite"); cell.querySelector(".control").insertAdjacentElement('beforeend', inputCounterDisplay); cell.setAttribute('maxChar', maxLength); rtfCountObserver.observe(cell, { attributes: true, childList: false, subtree: false }); }) /* -- RTF Placeholder Text -- */ function updateRTFPlaceholders() { const placeHolders = { 'en': "Enter Text...", 'fr': "Entrez le texte..." } const startPoll = Date.now(); const maxPoll = 5000; function setPlaceholderOnReady(editor) { const placeholder = placeHolders[LANG] || placeHolders["en"]; editor.config.editorplaceholder = placeholder; editor.config.rteplaceholder = placeholder; if (editor?.getData) { editor.setData(editor.getData()); } } function execWhenReady() { instanceKeys = Object.keys(CKEDITOR.instances); if (instanceKeys.length === RTFS.length) { for (key in CKEDITOR.instances) { setPlaceholderOnReady(CKEDITOR.instances[key]); } clearInterval(pollingINT); } else if (Date.now() - startPoll > maxPoll) { clearInterval(pollingINT); console.warn("Ckeditor text not updated"); } } const pollingINT = setInterval(execWhenReady, 200); } if (RTFS.length > 0) { updateRTFPlaceholders(); } /* Template includes script to add validators to Rich Text Fields to validate a specific scenario Scenario: The user types in anything on the field, the user leaves the field, and goes back to it and deletes their text The field isn't empty - there is a hidden field that is related to that rich text, and this hidden field is left with "" as a value which is not considered as empty by the default validator */ RTFTargets = '.form-control-cell:has([data-pcf-control*="MscrmControls.RichTextEditor.RichTextEditorControl"])'; var RTFTds = document.querySelectorAll(RTFTargets); const MAXCHAR_ERROR_TEXT = { 'en': "" } RTFTds.forEach((rtfTd) => { var fieldId = rtfTd.querySelector('input[type="hidden"]').id if (typeof (Page_Validators) == 'undefined') return; var fieldLabel = document.querySelector('#' + fieldId + '_label').textContent; /* -- Empty Check */ var richTextFieldEmptyValidator = document.createElement('span'); richTextFieldEmptyValidator.style.display = "none"; richTextFieldEmptyValidator.id = fieldId + "_validator"; richTextFieldEmptyValidator.errormessage = "" + fieldLabel + " "; richTextFieldEmptyValidator.controltovalidate = fieldId; richTextFieldEmptyValidator.validationGroup = ""; // Set this if you have set ValidationGroup on the form richTextFieldEmptyValidator.initialvalue = ""; // Function to validate field is empty - if not clear it and return true richTextFieldEmptyValidator.evaluationfunction = function () { if (document.querySelector('#' + fieldId).value === '\"\"') { return false; } else return true; }; // Add the new validator to the page validators array: Page_Validators.push(richTextFieldEmptyValidator); /* -- Max Char Check */ var richTextFieldMaxCharValidator = document.createElement('span'); richTextFieldMaxCharValidator.style.display = "none"; richTextFieldMaxCharValidator.id = fieldId + "_validator"; richTextFieldMaxCharValidator.targetID = `${fieldId}`; richTextFieldMaxCharValidator.targetCell = document.getElementById(fieldId).closest('.cell'); richTextFieldMaxCharValidator.maxChar = parseInt(richTextFieldMaxCharValidator.targetCell.getAttribute('maxchar')); if (LANG === 'fr') { richTextFieldMaxCharValidator.errormessage = "\"" + fieldLabel + `\" est supérieur à la limite de caractères ${richTextFieldMaxCharValidator.maxChar}`; } else { richTextFieldMaxCharValidator.errormessage = "\"" + fieldLabel + `\" is over the ${richTextFieldMaxCharValidator.maxChar} character limit.`; } richTextFieldMaxCharValidator.controltovalidate = fieldId; richTextFieldMaxCharValidator.validationGroup = ""; // Set this if you have set ValidationGroup on the form richTextFieldMaxCharValidator.initialvalue = ""; // Function to validate field is empty - if not clear it and return true richTextFieldMaxCharValidator.evaluationfunction = function () { var count = this.targetCell.getAttribute('charcount'); return (count <= this.maxChar); }; // Add the new validator to the page validators array: Page_Validators.push(richTextFieldMaxCharValidator); }); /* --- PATCHES --- */ // RTF RACE if (RTFTds?.length > 0 && UpdateButton) { let func = UpdateButton.getAttribute('onclick'); var existingListener = UpdateButton.onclick || function () { }; UpdateButton.onclick = null; UpdateButton.addEventListener("click", function (event) { event.preventDefault(); setTimeout(function () { existingListener.call(UpdateButton, event) }, 600); }); } } catch {} try { /* --- Subgrids/Datables --- */ /* https://wet-boew.github.io/wet-boew/docs/ref/tables/tables-en.html */ createActionHTML = ``; let subgrid_trunication_length = 90; // Global config for truncation length const getLanguageTagFromLinks = (str) => /\(en\)/.test(str) && /\(fr\)/.test(str) ? "dual" : /\(en\)/.test(str) ? "en" : /\(fr\)/.test(str) ? "fr" : "none"; let subgrids_preload_entity = document.querySelectorAll('.subgrid-cell .entity-grid, .entitylist .entity-grid') subgrids_preload_entity.forEach((s) => { $(s).one("loaded", (event) => { const cell = event.target.closest('.cell'); const table = event.target.querySelector('table'); const head_cols = table.querySelectorAll('thead > tr > th'); const rows = table.querySelectorAll('tr'); const colGroups = {}; //colGroups[base][cellLang] const sortDisabledHeaders = table.querySelectorAll('th.sort-disabled'); event.target.parentElement.table = table; // Shortcut for logical_name selectors const toolbar = event.target.querySelector('.toolbar-actions'); const info = cell?.querySelector('.info'); if (info && toolbar) { info.insertAdjacentElement('beforeend', toolbar); } table.dataset.columnCount = head_cols.length; table.style.setProperty('--column-count', head_cols.length); head_cols.forEach((col, index) => { col.querySelector('.fa')?.remove() let link = col.querySelector('a'); if (link) { let colLang = getLanguageTagFromLinks(link.innerText) col.dataset.colLang = colLang; col.dataset.colIndex = index; link.innerHTML = link?.innerHTML.replace(/[\(\[].*?[\)\]]/g, '').trim(); const lockedValue = '-1'; // Initially set the attribute col.setAttribute('tabindex', lockedValue); // Set up the observer const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.type === 'attributes' && mutation.attributeName === 'tabindex') { const currentVal = col.getAttribute('tabindex'); if (currentVal !== lockedValue) { col.setAttribute('tabindex', lockedValue); } } }); }); observer.observe(col, { attributes: true, attributeFilter: ['tabindex'], }); } let SR = col.querySelector(`.sr-only`); if (SR) { SR.classList.add('sr-only-focusable'); SR.classList.remove('sr-only'); } }); if (sortDisabledHeaders) { setTimeout(function() { sortDisabledHeaders.forEach(th => { const ariaLabel = th.getAttribute('aria-label'); if (ariaLabel && ariaLabel.includes(':')) { const cleanedLabel = ariaLabel.split(':')[0].trim(); th.setAttribute('aria-label', cleanedLabel); } const sortingCnt = th.querySelector('.sorting-cnt'); if (sortingCnt) { sortingCnt.remove(); } }); },800); } // Unburied Menu - Logic for determining if a subgrids action items should be "unburied" let dualsLinksCheck = event.target.querySelector('ul.dropdown-menu:has(li)'); let dropdown_count = []; let menus_lists = event.target.querySelectorAll('ul.dropdown-menu'); menus_lists.forEach((el, index) => { dropdown_count.push(el.children.length); }); s.classList.add("unburied-menu"); //add class to signal menu should be unburied (this is persistent on table reload) if (s.parentElement?.info?.innerText == '') { s.parentElement.info.remove(); } let creates = document.querySelectorAll('.action.create-action:not(:has(.fa))'); creates.forEach(el => el.insertAdjacentHTML('afterbegin', createActionHTML)); //_en and _fr pairMatched show/hide based on LANG if (DEVMODE != true) { rows.forEach((row) => { row.querySelectorAll("td[data-attribute]").forEach((col) => { const attr = col.getAttribute('data-attribute'); const match = attr.match(/^(.*)_(en|fr|EN|FR)$/); // Detect _en or _fr suffix if (match) { const base = match[1]; const cellLang = match[2]; const colIndex = col.cellIndex; const headCell = head_cols[colIndex]; colGroups[base] = colGroups[base] || {}; colGroups[base][cellLang] = {headCell: headCell, cell: col, row: row }; // Track column-cell pairs } }); }); // Phase 3: Determine columns to remove const removeLang = (LANG === 'en') ? 'fr' : 'en'; // Decide language to hide const columnsToRemove = new Set(); // Step 3.1: Use explicit (en) and (fr) from headers head_cols.forEach((col, index) => { if (col.dataset.colLang === removeLang) { columnsToRemove.add(index); // Add column index for removal } }); // Step 3.2: Fallback to _en/_fr suffix in rows if no explicit header Object.keys(colGroups).forEach((base) => { const group = colGroups[base]; if (group.en && group.fr) { // Removal const REMOVE_COL_POSITION = group[removeLang].cell.cellIndex; if (!columnsToRemove.has(REMOVE_COL_POSITION)) { columnsToRemove.add(REMOVE_COL_POSITION); // Add fallback column index } // Sorting if (group[removeLang]?.headCell.classList.contains('sort')) { if (group[removeLang]?.headCell.classList.contains('sort-desc')) { group[LANG]?.headCell.querySelector('a')?.click(); } group[LANG]?.headCell.querySelector('a')?.click(); } } }); // Step 4: Remove determined columns (headers + rows) [...columnsToRemove] // Convert Set to Array to allow reverse iteration .sort((a, b) => b - a) // Sort indices in descending order .forEach((colIndex) => { // head_cols[colIndex]?.remove(); // Remove header rows.forEach((row) => { try { row.deleteCell(colIndex); // Remove corresponding column cells } catch { } }); }); } table.classList.add('init-complete', 'fade-in'); // Mark the table as initialized }); /* - Enable Keyboard Nav for unburied menu (pairs with CSS)-- */ $(s).on("loaded", (event) => { const table = event.target.querySelector('table'); const headers = table.querySelector('thead > tr ').children; const rows = table.querySelectorAll('tr'); // Accesibility Fix for unburied menus event.target.querySelectorAll(".unburied-menu .dropdown-menu a").forEach((a) => a.setAttribute("tabindex", 0)); if ( event.target.classList.contains('hide-table-on-empty') ) { if(table.countRows() == 0) { table.closest('.view-grid').style.display = 'none'; } else { table.closest('.view-grid').style.display = ''; } } // Long text handler for subgrids event.target.querySelectorAll("td[data-type='System.String']").forEach((stringCol) => { if (stringCol.innerText.length > subgrid_trunication_length) { stringCol.classList.add('subgrid-popover'); new SubgridPopover(stringCol, s, subgrid_trunication_length, // Trunication length 600, //width 200, //duration 'click', //mode 600, //delay true //act as group ); } }); }); }); document.querySelectorAll(`.info.required label`).forEach((el) => { el.setAttribute("aria-required", true) }); setTimeout(() => { $(".entity-grid table").trigger("wb-init.wb-tables"); }, 2300); document.addEventListener('DOMContentLoaded', (event) => { document.querySelectorAll('.entity-grid.subgrid[class*="min-rec-"],.entity-grid.subgrid[class*="max-rec-"]') .forEach((innerSubgrid)=>{ const subgrid = innerSubgrid.parentElement; const classArr = [...innerSubgrid.classList]; const minClassName = classArr.find(cls => cls.startsWith('min-rec-')); const min = minClassName ? parseInt(minClassName.split('-').pop()) : 0; const maxClassName = classArr.find(cls => cls.startsWith('max-rec-')); const max = maxClassName ? parseInt(maxClassName.split('-').pop()) : Infinity; if (typeof min === 'number' && typeof max === 'number') { new SubgridValidator({ subgridElement: subgrid, minValidRows: min, maxValidRows: max, }); } }); }); HTMLTableElement.prototype.insertColumn = function (pos, title, content) { const rows = this.rows; const headerCell = document.createElement('th'); headerCell.innerHTML = title; headerCell.setAttribute('aria-label', title); for (let i = 0; i < rows.length; i++) { const row = rows[i]; let cell; if (i === 0) { // Header row cell = row.insertCell(pos); row.insertBefore(headerCell, cell); cell.remove(); } else { cell = row.insertCell(pos); // Preserve special markup (e.g., CRM links, icons) if (cell.innerHTML && cell.innerHTML.trim() !== '' && cell.innerHTML.trim() !== content.trim()) { continue; } // Store full text for potential truncation/popover cell.setAttribute("data-value", content); } } }; HTMLTableElement.prototype.iterateRows = function (callback) { for (let i = 0; i < this.rows.length; i++) { if (i === 0 && this.tHead) continue; callback(this.rows[i]); } }; HTMLTableElement.prototype.countRows = function () { return this.querySelectorAll("tbody tr[data-entity]").length; }; class SubgridPopover { static cssInjected = false; static activePopovers = new Set(); static activeClass = "popover-active"; static globalGroupActive = false; static getGlobalGroupActive() { return SubgridPopover.globalGroupActive; } static setGlobalGroupActive(value) { SubgridPopover.globalGroupActive = value; SubgridPopover.activePopovers.forEach(pop => pop.updateEventListeners()); } constructor(targetElement, parentElement, maxLength = 60, maxWidth = 450, animationDuration = 200, triggerType = 'hover', hoverDelay = 500, useGlobalGroup = true) { this.targetElement = targetElement; this.parentElement = parentElement; this.maxLength = maxLength; // Truncate length (passed from GlobalDeferred.js) this.maxWidth = maxWidth; this.animationDuration = animationDuration; this.triggerType = triggerType; this.hoverDelay = hoverDelay; this.useGlobalGroup = useGlobalGroup; this.popoverElement = null; this.showTimeout = null; this.hideTimeout = null; this._dismissed = false; if (this.triggerType === 'click' && this.useGlobalGroup) { SubgridPopover.globalGroupActive = false; } this.injectCSS(); this.setupText(); // Truncate text and set popover if needed if (!this.targetElement._boundClick) { this.targetElement.addEventListener("click", (e) => this.handleTargetClick(e)); this.targetElement._boundClick = true; } this.updateEventListeners(); document.addEventListener("click", (e) => this.handleOutsideClick(e)); // Close on outside click } injectCSS() { if (SubgridPopover.cssInjected) return; const style = document.createElement("style"); style.setAttribute("data-popover-css-injected", "true"); style.textContent = ` .custom-popover { background: white; border: 1px solid #ccc; padding: 10px; border-radius: 4px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); z-index: 9999; opacity: 0; transform: scale(0.99); transition: opacity ${this.animationDuration}ms, transform ${this.animationDuration}ms ease-out; position: absolute; overflow: auto; word-wrap: break-word; } .custom-popover.show { opacity: 1; transform: scale(1); } .popover-active { background-color: rgba(0, 0, 0, 0.1); outline: 2px solid #007bff; border-radius: 4px; } .popover-hover { cursor: pointer; } .subgrid-popover { cursor: pointer !important; /* Clickable cursor */ } `; document.head.appendChild(style); SubgridPopover.cssInjected = true; } setupText() { let fullText = this.targetElement.innerText.trim(); if (fullText.length > this.maxLength) { this.targetElement.setAttribute("data-value", fullText); this.targetElement.innerText = fullText.substring(0, this.maxLength) + '...'; this.targetElement.classList.add('subgrid-popover'); } } updateEventListeners() { if (this.useGlobalGroup && !this._dismissed && SubgridPopover.getGlobalGroupActive()) { this.targetElement.addEventListener("mouseenter", this.handleMouseEnter); this.targetElement.addEventListener("mouseleave", this.handleMouseLeave); } else if (!this.useGlobalGroup) { this.targetElement.addEventListener("mouseenter", this.handleMouseEnter); this.targetElement.addEventListener("mouseleave", this.handleMouseLeave); } } handleTargetClick(event) { event.stopPropagation(); if (this.useGlobalGroup) { SubgridPopover.setGlobalGroupActive(true); } this.dismissed = false; if (!this.popoverElement) { this.createPopover(); } else { this.destroy(true); } } createPopover() { if (this.triggerType === 'click' && this.useGlobalGroup) { if (!SubgridPopover.getGlobalGroupActive()) return; } if (this.popoverElement) return; SubgridPopover.closeAllPopovers(); let popoverContent = this.targetElement.getAttribute("data-value") || "No content"; this.popoverElement = document.createElement("div"); this.popoverElement.classList.add("custom-popover"); this.popoverElement.style.maxWidth = `${this.maxWidth}px`; this.popoverElement.innerHTML = popoverContent; document.body.appendChild(this.popoverElement); this.determinePosition(); setTimeout(() => this.popoverElement?.classList.add("show"), 10); SubgridPopover.activePopovers.add(this); this.targetElement.classList.add(SubgridPopover.activeClass); } determinePosition() { const tRect = this.targetElement.getBoundingClientRect(); this.popoverElement.style.left = `${tRect.left + window.scrollX}px`; this.popoverElement.style.top = `${tRect.bottom + window.scrollY + 5}px`; } handleOutsideClick(event) { if (!this.popoverElement || this.targetElement.contains(event.target) || this.popoverElement.contains(event.target)) { return; } this.destroy(true); } destroy(explicitDismiss = false) { if (this.popoverElement) { this.popoverElement.classList.remove("show"); setTimeout(() => { this.popoverElement?.remove(); this.popoverElement = null; }, this.animationDuration); } SubgridPopover.activePopovers.delete(this); this.targetElement.classList.remove(SubgridPopover.activeClass); } static closeAllPopovers(explicitDismiss = true) { SubgridPopover.activePopovers.forEach(pop => pop.destroy(explicitDismiss)); SubgridPopover.activePopovers.clear(); } } } catch {} try { // Removing Phone Number Placeholders document.querySelectorAll('input[type="text"][placeholder="Provide a telephone number"]').forEach(el => { el.removeAttribute('placeholder');}); document.querySelectorAll('input[type="text"][placeholder="Indiquez un numéro de téléphone"]').forEach(el => {el.removeAttribute('placeholder');}); } catch {} /* ----------------------------- */ /* Shared Control State Layer */ /* ----------------------------- */ // Used to unify interactions across choices, booleans, selects and radios on the portal. // Reason: Microsoft provides many different controls in the system which render slightly differently on the portal (CRM vs Portal, PCF vs BASIC FORM METADATA OVERRIDE, ETC) // and any code built to those controls as a result aren't interchangeable. IE: there's no direct "state" representation of the underneath, but instead a UI driven layer on top of that state. // This attempts to recific those permutations back to a simpler representation and also has enabled the ability for a tertiary state for booleans (yes/no/unselected) as a check of consent on the end user. // Note it uses getters and setters to sync code back/forth in order to represent a state. // See: /dev/forms-tests/basic-data-types/ for a playground of testing this functionality. // Configuration for different boolean classes, with special support for a tertiary state const booleanConfigs = { 'boolean-radio': { get: (element) => { return parseInt(element.querySelector(':checked').value) == true; }, set: (element, newValue) => { if (newValue == true) { element.querySelector('input[value="1"]').checked = true; } else if (newValue == false) { element.querySelector('input[value="0"]').checked = true; } } }, 'CoreControls.Checkbox': { get: (element) => {return element.value == 'true'}, set: (element, newValue) => element.closest('.control').querySelector('input[type="checkbox"]').checked = newValue }, 'boolean-radio-unselected': { get: (element) => { if(element.querySelector(':checked')) { return parseInt(element.querySelector(':checked').value) == true; } else { return null; } }, set: (element, newValue) => { if (newValue == true) { element.querySelector('input[value="1"]').checked = true; } else if (newValue == false) { element.querySelector('input[value="0"]').checked = true; } else if (newValue == 'unselected' || newValue == null) { element.querySelector('input:checked').checked = false; } } }, }; // Factory function to create boolvalue property with independant getters and setters. Note that this is only used so booleanConfigs can be well structured with dynamic getters/setters. // If you wish to use Object.defineProperty without swappable getters/setters, you can more sussinctly use the notation {get: ()=>{return this.value;}, set: (newValue)=>{....}} function createBooleanValueProperty(element, functionalOverrides) { const { get, set } = functionalOverrides; element.dataset.customProperties = 'boolvalue'; Object.defineProperty(element, 'boolvalue', { get: function() { return get(element); }, set: function(newValue) { set(element, newValue); } }); } // Boolean Radio - Native document.querySelectorAll(".boolean-radio").forEach(el=>createBooleanValueProperty(el, booleanConfigs['boolean-radio'])); // PowerApps.CoreControls.Checkbox - Native control with PCF added document.querySelectorAll(".form-control-cell .control:has([data-pcf-control*='PowerApps.CoreControls.Checkbox']) > input[type=hidden]") .forEach((el)=>{ el.cell.classList.add('PowerApps.CoreControls.Checkbox'); createBooleanValueProperty(el, booleanConfigs['CoreControls.Checkbox']) }); // Boolean Radio Implicit Unselected - GAC standardized single choice picklist (no default) w/ a radio basic form metadata override document.querySelectorAll('.cell.picklist-cell .control > .picklist:has(input[type="radio"][value="0"]):has(input[type="radio"][value="1"])') .forEach((RadioSpanWrapperEL)=>{ if(RadioSpanWrapperEL.querySelectorAll('input').length == 2) { //Confirmed as boolean-radio-implicitly-unselected RadioSpanWrapperEL.cell.classList.add('boolean-radio-implicitly-unselected'); createBooleanValueProperty(RadioSpanWrapperEL, booleanConfigs['boolean-radio-unselected']); } }) // List Based Abstractions - RadioSpan.value document.querySelectorAll('.cell.picklist-cell .control > .picklist') .forEach((RadioSpanWrapperEL)=>{ if(RadioSpanWrapperEL.dataset.customProperties) { RadioSpanWrapperEL.dataset.customProperties.concat('value'); } else { RadioSpanWrapperEL.dataset.customProperties = 'value'; } Object.defineProperty(RadioSpanWrapperEL, 'value', { get: function() { const checkedEl = this.querySelector(':checked'); if (checkedEl == null) return null; else { return parseInt(checkedEl.value); } }, set: function(newValue) { const elToCheck = this.querySelector('input[value="'+ newValue +'"]'); if (elToCheck) { elToCheck.checked = true; } else { console.log('Value not found') } } }) }); document.querySelectorAll('.cell.money .text.money.form-control') .forEach((moneyEL)=>{ if(moneyEL.dataset.customProperties) { moneyEL.dataset.customProperties.concat('floatValue'); } else { moneyEL.dataset.customProperties = 'floatValue'; } Object.defineProperty(moneyEL, 'floatValue', { get: function() { return toFloatLocaleAware(this.value); }, }) }); })();