0

I wrote a different version of the salesforce lightning rich text input lwc which forbids image uploads, font changes, and color changes. I created custom buttons which only allow users to color their text red or black, and also made a custom font size change dropdown. This all works amazingly in my sandbox but in production the buttons just delete the highlighted text. I have tried everything but can't seem to figure out how I can make this available in production. It has something to do with the dom manipulation security from what I understand. I get the error "access denied: {"from":{"namespace":"c"}}" at runtime. Here is my code.

js:

import { LightningElement, api, track } from 'lwc';

export default class inputRichTextFSCRedBlack extends LightningElement {

    //Input and Output Attributes for Flow
    @api value; //set empty in connectedCallback if undefined.
    @api disableAdvancedTools = false;
    @api disallowedWordsList;
    @api disallowedSymbolsList;
    @api autoReplaceMap;
    @api warnOnly = false;
    @api label;
    @api characterLimit;
    @api required = false;
    @api disabledCategories = '';
    @api enabledFormats;

    formats = ['size', 'bold', 'italic', 'underline',
        'strike', 'list', 'indent', 'align', 'link',
        'clean', 'table', 'header','background','code','code-block','script','blockquote','direction'];

    fontSizes = [
        { label: '10', value: '10px' },
        { label: '11', value: '11px' },
        { label: '12', value: '12px' },
        { label: '13', value: '13px' },
        { label: '14', value: '14px' },
        { label: '16', value: '16px' },
        { label: '18', value: '18px' },
        { label: '24', value: '24px' },
        { label: '36', value: '36px' },
        { label: '48', value: '48px' },
        { label: '72', value: '72px' }
    ];


        colorRed() {
            try {
                const selection = window.getSelection();
                if (selection.rangeCount > 0) {
                    const range = selection.getRangeAt(0);
                    const span = document.createElement('span');
                    span.style.color = 'red';
                    span.appendChild(range.extractContents());
                    range.insertNode(span);
                } else {
                    throw new Error('No text selected.');
                }
            } catch (error) {
                this.handleError('colorRed', error);
            }
        }

        colorBlack() {
            const selection = window.getSelection();
            if (selection.rangeCount > 0) {
                const range = selection.getRangeAt(0);
                const span = document.createElement('span'); 1 
                span.style.color = 'black';
                span.appendChild(range.extractContents());
                range.insertNode(span);
            }
        }

        handleFontSizeChange(event) {
            const selectedSize = event.target.value;
            this.applyFontSize(selectedSize);
        }
    
        applyFontSize(size) {
            const selection = window.getSelection();
            if (selection.rangeCount > 0) {
                const range = selection.getRangeAt(0);
                const span = document.createElement('span');
                span.style.fontSize = size;
                span.appendChild(range.extractContents());
                range.insertNode(span);
            }
        }
    
        

    //Validation hook to use standard in Flow.  Only enforce if Advanced Tools are enabled
    @api validate(){
        if(!this.disableAdvancedTools){
            this.value = this.richText;
        }

        let errorMessage = "You must make a selection in: " + this.label + " to continue";
        if (this.required === true && !this.value) {
            return {
                isValid: false,
                errorMessage: errorMessage
            };
        }

        if(this.disableAdvancedTools || this.warnOnly){
            return {isValid:true};
        }else if(this.characterCap && (this.characterCount > this.characterLimit)){
            //failure scenario so set tempValue in sessionStorage
            sessionStorage.setItem('tempValue',this.value);
            return {
                isValid:false,
                errorMessage: 'Cannot Advance - Character Limit Exceeded: '+this.characterCount + ' > ' + this.characterLimit
            };
        }else if(!this.isValidCheck){
            //failure scenario so set tempValue in sessionStorage
            sessionStorage.setItem('tempValue',this.value);
            return {
                isValid:false,
                errorMessage: 'Cannot Advance - Invalid Symbols/Words Remain in Rich Text: '+this.runningBlockedInput.toString()
            };
        }else{
            return {isValid:true};
        }
    }

    //Other Variables
    @track richText; //use separate variable for richerText as value attribute is causing conflict
    @track disallowedWordsArray = [];
    @track disallowedWords;
    @track disallowedSymbolsArray = [];
    @track disallowedSymbols;
    @track searchTerm = '';
    @track replaceValue = '';
    @track interimValue = '';
    @track symbolsNotAllowed;
    @track wordsNotAllowed;
    @track oldRichText;
    @track dirty = false;
    @track autoReplaceEnabled = false;
    @track runningBlockedInput = [];
    @track searchButton = false;
    @track isValidCheck = true;
    @track errorMessage;
    @track characterCount = 0;
    @track characterCap = false;
    replaceMap = {};
    regTerm = '';
    applyTerm = '';
    instructions = '1)  Find and Replace:  Use Magnifying Glass, Enter Terms and Use Check Mark.  '+
                    '2)  Auto Replace:  If your Admin has configured it, Use Merge Icon to replace suggested terms.';

    //Begin functionality

    //Set initial values on load
    connectedCallback() {
        
        //use sessionStorage to fetch and restore latest value before validation failure.
        if(sessionStorage){
            if(sessionStorage.getItem('tempValue')){
                this.value = sessionStorage.getItem('tempValue');
                sessionStorage.removeItem('tempValue'); //clear value after selection
            }
        }

        if(this.enabledFormats){
            this.formats = this.enabledFormats.split(',').map(function(item) {
                return item.trim();
            });
        } 

        if(!this.disableAdvancedTools){
            console.log('disableAdvancedTools is false, in connected callback');
            (this.value != undefined) ? this.richText = this.value : this.richText = '';
            this.characterCount = this.richText.length;
            if(this.disallowedSymbolsList != undefined){
                this.disallowedSymbolsArray = this.disallowedSymbolsList.replace(/\s/g,'').split(',');
                for(let i=0; i<this.disallowedSymbolsArray.length; i++){
                    if(i == 0){
                        if(this.disallowedSymbolsArray.length != 1){
                            this.disallowedSymbols = '['+ this.disallowedSymbolsArray[i] + '|';
                        }else{
                            this.disallowedSymbols = '['+ this.disallowedSymbolsArray[i] + ']';
                        }
                    } else if (i == (this.disallowedSymbolsArray.length - 1)){
                        this.disallowedSymbols = this.disallowedSymbols.concat(this.disallowedSymbolsArray[i] + ']');
                    } else {
                        this.disallowedSymbols = this.disallowedSymbols.concat(this.disallowedSymbolsArray[i] + '|');
                    }
                }
            }
    
            if(this.disallowedWordsList != undefined){
                this.disallowedWordsArray = this.disallowedWordsList.replace(/\s/g,'').split(',');
                for(let i=0; i<this.disallowedWordsArray.length; i++){
                    if(i == 0){
                        if(this.disallowedWordsArray.length != 1){
                            this.disallowedWords = '('+this.disallowedWordsArray[i] + '|';
                        }else{
                            this.disallowedWords = '('+this.disallowedWordsArray[i] + ')\b';
                        }
                    } else if (i == (this.disallowedWordsArray.length - 1)){
                        this.disallowedWords = this.disallowedWords.concat(this.disallowedWordsArray[i] + ')\\b');
                    } else {
                        this.disallowedWords = this.disallowedWords.concat(this.disallowedWordsArray[i] + '|');
                    }
                }
            }
            if(this.disallowedSymbols != undefined) this.symbolsNotAllowed = new RegExp(this.disallowedSymbols,'g');
            if(this.disallowedWords != undefined) this.wordsNotAllowed = new RegExp(this.disallowedWords,'g');
            if(this.autoReplaceMap != undefined){
                this.replaceMap = JSON.parse(this.autoReplaceMap);
                this.autoReplaceEnabled = true;
            } 
            if(this.characterLimit > 0){
                this.characterCap = true;
            }
        }
    }
    
    //Handle updates to Rich Text field with no enhanced features
    handleValueChange(event) {
        this.value = event.target.value;
    }

    //Handle updates to Rich Text field with enhanced features
    handleTextChange(event) {
        this.runningBlockedInput = [];
        this.isValidCheck = true;
        if (this.symbolsNotAllowed != undefined || this.wordsNotAllowed != undefined) {
            this.interimValue = (event.target.value).toLowerCase();
            this.interimValue = this.interimValue.replace(/(<([^>]+)>)/ig, "");
            
            //Symbol check section
            if (this.symbolsNotAllowed != undefined) {
                let matchesSymbol = this.interimValue.match(this.symbolsNotAllowed);
                if (matchesSymbol != null && matchesSymbol.length > 0) {
                    for(let i = 0; i < matchesSymbol.length; i++){
                        this.runningBlockedInput.push(matchesSymbol[i]);
                    }
                    this.isValidCheck = false;
                } else {
                    this.richText = event.target.value;
                }
            }

            if (this.wordsNotAllowed != undefined) {
                let matchesWords = this.interimValue.match(this.wordsNotAllowed);
                if (matchesWords != null && matchesWords.length > 0) {
                    for(let i = 0; i < matchesWords.length; i++){
                        this.runningBlockedInput.push(matchesWords[i]);
                    }
                    this.isValidCheck = false;
                } else {
                    this.richText = event.target.value;
                }
            }
        } else {
            this.isValidCheck = true;
            this.richText = event.target.value;
        }
        this.characterCount = this.richText.length;
        if(this.characterCap && this.characterCount > this.characterLimit){
            this.isValidCheck = false;
        }
        //Display different message if warn only - validation also won't be enforced on Next.
        if(this.characterCap && this.characterCount > this.characterLimit){
            this.errorMessage = 'Error - Character Limit Exceeded';
        }else if(!this.warnOnly){
            this.errorMessage = 'Error - Invalid Symbols/Words found: '+this.runningBlockedInput.toString();
        }else{
            this.errorMessage = 'Warning - Invalid Symbols/Words found: '+this.runningBlockedInput.toString();
        }
        
    }

    //Set css on Character count if passing character limit
    get charClass(){
        return (this.characterLimit < this.characterCount ? 'warning' : '');
    }

    //Handle initiation of Search and Replace
    handleOpenSearch(event) {
        this.searchButton = !this.searchButton;
    }

    //Search and Replace Search for Value
    handleSearchChange(event) {
        this.searchTerm = (event.target.value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    }

    //Search and Replace Replace with Value
    handleReplaceChange(event) {
        this.replaceValue = (event.target.value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    }

    //Execute Search and REplace
    searchReplace() {
        this.oldRichText = this.richText;
        this.dirty = true;
        let draftValue = this.richText;
        this.searchTerm = this.escapeRegExp(this.searchTerm);
        this.replaceValue = this.escapeRegExp(this.replaceValue);
        draftValue = this.replaceAll(draftValue, this.searchTerm, this.replaceValue);
        this.richText = draftValue;
    }

    //Execute Auto-Replacement based on map.
    applySuggested(event) {
        this.oldRichText = this.richText;
        this.dirty = true;
        let draftValue = this.richText;
        let regTerm = '';
        for (var key in this.replaceMap) {
            this.applyTerm = this.replaceMap[key];
            this.regTerm = key;
            draftValue = this.replaceAll(draftValue, this.regTerm, this.applyTerm);
        }
        this.richText = draftValue;
    }

    //Replace All function helper
    replaceAll(str, term, replacement) {
        return str.replace(new RegExp(term, 'ig'), replacement);
    }

    //Undo last change
    handleRevert() {
        this.richText = this.oldRichText;
        this.dirty = false;
    }

    //Clean input for RegExp
    escapeRegExp(str) {
        return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    }
    

}

html:

<template>
<!-- Start Rich Text Input Section -   -->
<template if:true={disableAdvancedTools}>
    <div>
        <lightning-input-rich-text value={value} onchange={handleValueChange} formats={formats} label={label} required={required} label-visible="true" disabled-categories={disabledCategories}></lightning-input-rich-text>
    </div>
</template>
<!-- End Rich Text Input Section -->

<!-- Start Entry Enhancements - only display if enableAdvancedTools activated -->
<template if:false={disableAdvancedTools}>
    <div>
        <lightning-input-rich-text value={richText} formats={formats} label={label} onchange={handleTextChange} style="font-family: Arial"
            label-visible="true" valid={isValidCheck} message-when-bad-input={errorMessage} required={required} disabled-categories={disabledCategories}>
        </lightning-input-rich-text>
    </div>
    <div class="slds-grid slds-wrap slds-p-around_xx-small slds-m-bottom_x-small lgc-bg">
        <lightning-button-group>
            <div class="slds-p-around_xxx-small">
                <lightning-button-icon variant="border-filled" size="medium" icon-name="utility:search"
                    onclick={handleOpenSearch} alternative-text="Open Search and Replace"
                    tooltip="Open Search and Replace">
                </lightning-button-icon>
            </div>
            <template if:true={searchButton}>
                <div class="slds-p-around_xxx-small">
                    <lightning-input type="text" placeholder="Search Text" variant="label-hidden" 
                        value={searchTerm} onchange={handleSearchChange}></lightning-input>
                </div>
                <div class="slds-p-around_xxx-small">
                    <lightning-input type="text" placeholder="Replace with Text" variant="label-hidden"
                        value={replaceValue} onchange={handleReplaceChange}></lightning-input>
                </div>
                <div class="slds-p-left_xxx-small slds-p-right_small slds-p-top_xxx-small slds-p-bottom_xxx-small">
                    <lightning-button-icon variant="brand" size="medium" icon-name="utility:check"
                        onclick={searchReplace} alternative-text="Search and Replace" tooltip="Search and Replace">
                    </lightning-button-icon>
                </div>
            </template>
            <template if:true={autoReplaceEnabled}>
                <div class="slds-p-around_xxx-small">
                    <lightning-button-icon variant="border-filled" size="medium" icon-name="utility:merge"
                        onclick={applySuggested} alternative-text="Apply Suggested Terms" tooltip="Apply Suggested Terms">
                    </lightning-button-icon>
                </div>
            </template>
            <template if:true={dirty}>
                <div class="slds-p-around_xxx-small">
                    <lightning-button-icon variant="border-filled" size="medium" icon-name="utility:undo"
                        onclick={handleRevert} alternative-text="Undo Last Change" tooltip="Undo Last Change">
                    </lightning-button-icon>
                </div>
            </template>

            <template if:true={characterCap}>
                <div class="slds-p-left_x-small slds-p-right_xxx-small slds-p-top_x-small slds-p-bottom_xxx-small">
                    <p class={charClass}>{characterCount}/{characterLimit}</p>
                </div>
            </template>

            <div class="slds-p-left_x-small slds-p-right_xxx-small slds-p-top_xx-small slds-p-bottom_xxx-small">
                <lightning-helptext content={instructions}></lightning-helptext>
            </div>
        </lightning-button-group>

        <lightning-button-group>
            <div class="slds-p-around_xxx-small">
                <lightning-button-icon variant="border-filled" size="medium" icon-name="utility:stop" class="red-button"
                    onclick={colorRed} alternative-text="Color Red" tooltip="Color Red">
                </lightning-button-icon>
            </div>
            <div class="slds-p-around_xxx-small">
                <lightning-button-icon variant="border-filled" size="medium" icon-name="utility:stop" class="black-button"
                    onclick={colorBlack} alternative-text="Color Black" tooltip="Color Black">
                </lightning-button-icon>
            </div>
        </lightning-button-group>

        <lightning-button-group>
            <div class="slds-p-around_xxx-small">
                <select onchange={handleFontSizeChange} style="height: 32px; border-radius: 0.25rem; max-height: 200px; overflow-y: auto; padding-left:5px;">
                    <option value="" style="color: #181818; font-family: Arial;">Font Size</option>
                    <template for:each={fontSizes} for:item="size">
                        <option key={size.value} value={size.value}>{size.label}</option>
                    </template>
                </select>
            </div>
        </lightning-button-group>
    </div>
</template>
<!-- End Entry Enhancements -->
2
  • Have you, by coincidence, "Lightning Web Security" enabled on the Sandbox, but not on production? This would explain it, e.g. you use document.createElement(...). Commented Dec 6 at 11:44
  • 2
    @FelixvanHove that was it! I swear i went over that at one point and ruled it out but that was my exact situation. Thank you so much!!
    – ASK
    Commented Dec 6 at 16:28

0

You must log in to answer this question.