Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,13 @@ jobs:
run: npm install

- name: ESLint
run: npx eslint 'richTextField/v1/index.js' 'richTextFieldWithTables/v1/index.js'
run: npm run lint

- name: Prettier check
run: npx prettier --config prettierrc.json --check 'richTextField/v1/index.js' 'richTextFieldWithTables/v1/index.js'
run: npm run format:check

- name: Run tests
run: npx jest --coverage --ci
run: npm test -- --watchAll=false --ci

- name: npm audit
run: npm audit --audit-level=high
Expand Down
9 changes: 9 additions & 0 deletions cp/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@
"english_translations": "readonly",
"french_translations": "readonly"
},
"ignorePatterns": [
"*.min.js",
"*.config.js",
"**/i18n.js",
"summernote*.js",
"jquery*.js",
"tests/",
"node_modules/"
],
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "script"
Expand Down
5 changes: 5 additions & 0 deletions cp/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
*.min.js
summernote*.js
jquery*.js
node_modules/
coverage/
2 changes: 1 addition & 1 deletion cp/appian-component-plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<description>A simple rich text editor</description>
<support supported="false" email="richtext-componentpluginsupport@appian.com" />
<vendor name="Appian" url="https://www.appian.com"/>
<version>1.19.0</version>
<version>1.20.0</version>
</plugin-info>
<component rule-name="richTextField" version="1.0.0">
<sdk-version>2.0.0</sdk-version>
Expand Down
8 changes: 2 additions & 6 deletions cp/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,11 @@ module.exports = {
transform: {
// Use our custom transform for the source index.js files
"richTextField/v1/index\\.js$": "./tests/helpers/browserScriptTransform.js",
"richTextFieldWithTables/v1/index\\.js$":
"./tests/helpers/browserScriptTransform.js",
"richTextFieldWithTables/v1/index\\.js$": "./tests/helpers/browserScriptTransform.js",
},
// Don't transform node_modules, but DO transform our source files
transformIgnorePatterns: ["/node_modules/"],
collectCoverageFrom: [
"richTextField/v1/index.js",
"richTextFieldWithTables/v1/index.js",
],
collectCoverageFrom: ["richTextField/v1/index.js", "richTextFieldWithTables/v1/index.js"],
coverageDirectory: "coverage",
coverageReporters: ["text", "lcov", "clover"],
};
12 changes: 6 additions & 6 deletions cp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
"scripts": {
"test": "jest --coverage",
"test:watch": "jest --watch",
"lint": "eslint 'richTextField/v1/index.js' 'richTextFieldWithTables/v1/index.js'",
"format": "prettier --config prettierrc.json --write 'richTextField/v1/index.js' 'richTextFieldWithTables/v1/index.js'",
"format:check": "prettier --config prettierrc.json --check 'richTextField/v1/index.js' 'richTextFieldWithTables/v1/index.js'"
"lint": "eslint '**/*.js' --ignore-pattern '*.min.js'",
"format": "prettier --config prettierrc.json --write '**/*.js'",
"format:check": "prettier --config prettierrc.json --check '**/*.js'"
},
"devDependencies": {
"eslint": "8.57.1",
"@eslint/js": "8.57.1",
"prettier": "3.3.3",
"eslint": "8.57.1",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0"
"jest-environment-jsdom": "29.7.0",
"prettier": "3.3.3"
}
}
6 changes: 2 additions & 4 deletions cp/richTextField/v1/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ const english_translations = {
"The image storage connected system parameter is empty. Please update the parameter 'imageStorageConnectedSystem' with a valid connected system or set 'allowImages' to false.",
validationContentTooBig: "Content exceeds maximum allowed size",
validationConnectedSystemResponse: "Response from connected system:",
validationDocURLFailure:
"Unable to obtain the doc URL from the connected system",
validationDocURLFailure: "Unable to obtain the doc URL from the connected system",
default: "Default",
};
const french_translations = {
Expand Down Expand Up @@ -67,7 +66,6 @@ const french_translations = {
"Le paramètre du système connecté pour le stockage des images n'est pas renseigné. Veuillez mettre à jour le paramètre 'imageStorageConnectedSystem' en sélectionnant un système connecté valide ou mettre le paramètre 'allowImages' à faux.",
validationContentTooBig: "Ce contenu dépasse la taille maximum autorisée",
validationConnectedSystemResponse: "Réponse du système connecté :",
validationDocURLFailure:
"Impossible d'obtenir l'URL du document à partir du système connecté",
validationDocURLFailure: "Impossible d'obtenir l'URL du document à partir du système connecté",
default: "Réglage par défaut",
};
23 changes: 14 additions & 9 deletions cp/richTextField/v1/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ Appian.Component.onNewValue(function (allParameters) {
/* Skip if recently blurred */
if (!window.isQuillBlurred) {
/* Skip if an image is present that has not been converted to a file yet */
if (source == "user" && !doesBase64ImageExist(quill.getContents())) {
if (source === "user" && !doesBase64ImageExist(quill.getContents())) {
window.isQuillActive = true;
updateValue();
}
Expand Down Expand Up @@ -330,7 +330,7 @@ function updateValue() {
const contents = quill.getContents();
/* Save value (Quill always adds single newline at end, so treat that as null) */
/* Check getLength() in case an image is added without any text */
if (quill.getText() === "\n" && quill.getLength() == 1) {
if (quill.getText() === "\n" && quill.getLength() === 1) {
Appian.Component.saveValue("richText", null);
} else {
// Due to race conditions, we were saving out base64 images in some cases
Expand Down Expand Up @@ -380,7 +380,7 @@ function handleDisplay(enableProgressBar, height, placeholder) {
quillContainer.style.minHeight = "";
parentContainer.style.minHeight = "";
} else {
if (height == "auto") {
if (height === "auto") {
/* For "auto" height, start with a min height but allow to grow taller as content increases */
quillContainer.style.height = "auto";
parentContainer.style.height = "auto";
Expand Down Expand Up @@ -548,6 +548,7 @@ function uploadBase64Img(imageSelector) {
docID = response.payload.docID;

if (docURL == null) {
// eslint-disable-line eqeqeq
message = getTranslation("validationDocURLFailure");
console.error(message);
Appian.Component.setValidations(message);
Expand All @@ -571,7 +572,7 @@ function uploadBase64Img(imageSelector) {
}
}

base64Str = imageSelector.getAttribute("src");
var base64Str = imageSelector.getAttribute("src");
if (typeof base64Str !== "string" || base64Str.length < 100) {
return base64Str;
}
Expand Down Expand Up @@ -618,10 +619,10 @@ function getBrowserAndVersion() {
}
if (M[1] === "Chrome") {
tem = ua.match(/\b(OPR|Edge)\/(\d+)/);
if (tem != null) return tem.slice(1).join(" ").replace("OPR", "Opera");
if (tem !== null) return tem.slice(1).join(" ").replace("OPR", "Opera");
}
M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, "-?"];
if ((tem = ua.match(/version\/(\d+)/i)) != null) M.splice(1, 1, tem[1]);
if ((tem = ua.match(/version\/(\d+)/i)) !== null) M.splice(1, 1, tem[1]);
return M.join(" ");
}

Expand All @@ -632,8 +633,9 @@ function getBrowserAndVersion() {
function initializeCopyPaste() {
var browserArray = getBrowserAndVersion().split(" ");
var browser = browserArray[0];
// eslint-disable-next-line no-unused-vars
var browserVersion = browserArray[1];
if (browser != "Firefox" && browser != "Chrome") {
if (browser !== "Firefox" && browser !== "Chrome") {
var IMAGE_MIME_REGEX = /^image\/(p?jpeg|gif|png)$/i;
var loadImage = function (file) {
var reader = new FileReader();
Expand Down Expand Up @@ -665,11 +667,13 @@ function initializeCopyPaste() {
// - https://site-appiancloud.com/suite/sites/.... (IE)
// - https://site-appiancloud.com/ (Chrome, Firefox)
// - https://site-appiancloud.com (Safari)
// eslint-disable-next-line no-unused-vars
function returnParentWindowUrl() {
return document.referrer.match(/^.*(?=\/suite\/.*)|^.*(?=\/$)|^.*$/g)[0];
}

function translateToolbar() {
// eslint-disable-next-line no-unused-vars
var toolbar = document.getElementById("quill-toolbar");

var nodesToTranslate = document.querySelectorAll("[data-i18n]");
Expand All @@ -678,13 +682,14 @@ function translateToolbar() {
var node = nodeArray[i];
var i18nAttr = node.getAttribute("data-i18n");
var translatedValue;
var key;
if (i18nAttr === "innerText") {
var key = node.innerText;
key = node.innerText;
translatedValue = getTranslation(key);
if (!translatedValue) continue;
node.innerText = translatedValue;
} else {
var key = node.getAttribute(i18nAttr);
key = node.getAttribute(i18nAttr);
translatedValue = getTranslation(key);
if (!translatedValue) continue;
node.setAttribute(i18nAttr, translatedValue);
Expand Down
7 changes: 7 additions & 0 deletions cp/richTextFieldWithTables/v1/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,10 @@ button:focus {
outline-offset: -2px !important;
outline-width: 0.5px !important;
}
/* Override browser defaults for ins/del to let font color/strike handle the visual styling */
ins {
text-decoration: none;
}
del {
text-decoration: none;
}
14 changes: 10 additions & 4 deletions cp/richTextFieldWithTables/v1/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,26 @@ const english_translations = {
textHeaderMedium: "Medium Header",
textHeaderSmall: "Small Header",
textNormal: "Normal Text",
validationImageStorageConnectedSystemEmpty: "The image storage connected system parameter is empty. Please update the parameter 'imageStorageConnectedSystem' with a valid connected system or set 'allowImages' to false.",
validationImageStorageConnectedSystemEmpty:
"The image storage connected system parameter is empty. Please update the parameter 'imageStorageConnectedSystem' with a valid connected system or set 'allowImages' to false.",
validationContentTooBig: "Content exceeds maximum allowed size",
validationConnectedSystemResponse: "Response from connected system:",
validationDocURLFailure: "Unable to obtain the doc URL from the connected system",
default: "Default"
default: "Default",
added: "Added: ",
removed: "Removed: ",
};
const french_translations = {
textHeaderLarge: "Grand en-tête",
textHeaderMedium: "Moyen en-tête",
textHeaderSmall: "Petit en-tête",
textNormal: "Texte normal",
validationImageStorageConnectedSystemEmpty: "Le paramètre du système connecté pour le stockage des images n'est pas renseigné. Veuillez mettre à jour le paramètre 'imageStorageConnectedSystem' en sélectionnant un système connecté valide ou mettre le paramètre 'allowImages' à faux.",
validationImageStorageConnectedSystemEmpty:
"Le paramètre du système connecté pour le stockage des images n'est pas renseigné. Veuillez mettre à jour le paramètre 'imageStorageConnectedSystem' en sélectionnant un système connecté valide ou mettre le paramètre 'allowImages' à faux.",
validationContentTooBig: "Ce contenu dépasse la taille maximum autorisée",
validationConnectedSystemResponse: "Réponse du système connecté :",
validationDocURLFailure: "Impossible d'obtenir l'URL du document à partir du système connecté",
default: "Réglage par défaut"
default: "Réglage par défaut",
added: "Ajouté : ",
removed: "Supprimé : ",
};
67 changes: 58 additions & 9 deletions cp/richTextFieldWithTables/v1/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ const ALLOWED_TAGS = [
"em",
"u",
"strike",
"ins",
"del",
"sup",
"sub",
"font",
Expand Down Expand Up @@ -252,7 +254,7 @@ function buildEditor() {
.on("click", function (e) {
var selectedItem = $(this).html();
insertableItemsFiltered.map(function (i) {
if (selectedItem == cleanHtml(i.label, true)) {
if (selectedItem === cleanHtml(i.label, true)) {
context.invoke("editor.insertText", i.value);
}
});
Expand Down Expand Up @@ -437,7 +439,7 @@ function uploadBase64Img(imageSelector) {
}
}

base64Str = imageSelector.getAttribute("src");
var base64Str = imageSelector.getAttribute("src");
if (typeof base64Str !== "string" || base64Str.length < 100) {
return base64Str;
}
Expand Down Expand Up @@ -523,6 +525,49 @@ function isTextPresent(text) {
return html.includes(text);
}

/**
* Post-processes the readOnly DOM to replace <ins> and <del> elements with
* aria-labeled <span> elements. This prevents VoiceOver from double-reading
* the content while still announcing "added:" or "removed:" for screen readers.
* Only affects the rendered DOM — the stored richText value is never modified.
*/
function makeInsDelAccessible() {
var container = document.getElementById("summernote");
if (!container) return;

container.querySelectorAll("ins").forEach(function (el) {
var span = document.createElement("span");
span.setAttribute("role", "img");
span.setAttribute("aria-label", getTranslation("added") + escapeAttr(el.textContent));
if (el.getAttribute("style")) {
span.setAttribute("style", el.getAttribute("style"));
}
span.innerHTML = el.innerHTML;
el.replaceWith(span);
});

container.querySelectorAll("del").forEach(function (el) {
var span = document.createElement("span");
span.setAttribute("role", "img");
span.setAttribute("aria-label", getTranslation("removed") + escapeAttr(el.textContent));
if (el.getAttribute("style")) {
span.setAttribute("style", el.getAttribute("style"));
}
span.innerHTML = el.innerHTML;
el.replaceWith(span);
});
}

/**
* Escapes double quotes in a string for safe use in HTML attribute values.
* @param {string} str - The string to escape
* @return {string} The escaped string
*/
function escapeAttr(str) {
if (!str) return "";
return str.replace(/"/g, "&quot;");
}

/**
* Updates the editor content HTML value from the Appian SAIL parameter, only updating if there is a change
*/
Expand All @@ -532,6 +577,9 @@ function setEditorContents() {
// Then immediately destroy since setting the contents creates it
summernote.summernote("code", cleanHtml(window.allParameters.richText));
summernote.summernote("destroy");
// Post-process for accessibility: replace <ins>/<del> with aria-labeled spans
// to prevent VoiceOver double-reading while maintaining screen reader announcements
makeInsDelAccessible();
} else {
// Otherwise, only update the contents if they've actually changed to avoid triggering the onChange event
if (
Expand Down Expand Up @@ -602,18 +650,18 @@ function setDynamicCss() {
function setA11yCss() {
// set aria-hidden to false for the close buttons
var close_buttons = document.getElementsByClassName("btn-close");
for (var i = 0; i < close_buttons.length; i++) {
close_buttons[i].setAttribute("aria-hidden", "false");
for (var j = 0; j < close_buttons.length; j++) {
close_buttons[j].setAttribute("aria-hidden", "false");
}
// set aria-expanded to false for buttons that will expand
var dropdowns = document.querySelectorAll('[data-bs-toggle="dropdown"]');
for (var i = 0; i < dropdowns.length; i++) {
dropdowns[i].setAttribute("aria-expanded", "false");
for (var k = 0; k < dropdowns.length; k++) {
dropdowns[k].setAttribute("aria-expanded", "false");
}
// set aria-label to "formatting options" for toolbars
var toolbars = document.querySelectorAll('[role="toolbar"]');
for (var i = 0; i < toolbars.length; i++) {
toolbars[i].setAttribute("aria-label", "formatting options");
for (var m = 0; m < toolbars.length; m++) {
toolbars[m].setAttribute("aria-label", "formatting options");
}
}

Expand Down Expand Up @@ -852,6 +900,7 @@ function readClipboard(e) {
}
}

// eslint-disable-next-line no-unused-vars
function handleImagePasteFromFile(e) {
var clipboardData = e.originalEvent.clipboardData;
var items = clipboardData.items;
Expand Down Expand Up @@ -897,7 +946,6 @@ function isInternetExplorer() {
var ua = window.navigator.userAgent;
var msie = ua.indexOf("MSIE ");
msie = msie > 0 || !!navigator.userAgent.match(/Trident.*rv\:11\./);
var ffox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
return msie;
}

Expand All @@ -906,6 +954,7 @@ function isInternetExplorer() {
* @param {function} func - Function to run on a delay
* @param {integer} delay - MS to delay re-execution of the function
*/
// eslint-disable-next-line no-unused-vars
function debounce(func, delay) {
var inDebounce;
return function () {
Expand Down
Loading
Loading