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 -->
document.createElement(...)
.