Skip to content

Commit 1ab87d1

Browse files
authored
fix: default field export background transparent, allow config on export (#770)
1 parent 7748de9 commit 1ab87d1

5 files changed

Lines changed: 140 additions & 7 deletions

File tree

packages/super-editor/src/core/Editor.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1553,6 +1553,7 @@ export class Editor extends EventEmitter {
15531553
exportXmlOnly = false,
15541554
comments = [],
15551555
getUpdatedDocs = false,
1556+
fieldsHighlightColor = null,
15561557
} = {}) {
15571558
// Pre-process the document state to prepare for export
15581559
const json = this.#prepareDocumentForExport(comments);
@@ -1567,6 +1568,7 @@ export class Editor extends EventEmitter {
15671568
comments,
15681569
this,
15691570
exportJsonOnly,
1571+
fieldsHighlightColor,
15701572
);
15711573

15721574
if (exportXmlOnly || exportJsonOnly) return documentXml;

packages/super-editor/src/core/super-converter/SuperConverter.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,7 @@ class SuperConverter {
409409
comments = [],
410410
editor,
411411
exportJsonOnly = false,
412+
fieldsHighlightColor,
412413
) {
413414
const commentsWithParaIds = comments.map((c) => prepareCommentParaIds(c));
414415
const commentDefinitions = commentsWithParaIds.map((c, index) =>
@@ -423,6 +424,7 @@ class SuperConverter {
423424
commentsExportType,
424425
isFinalDoc,
425426
editor,
427+
fieldsHighlightColor,
426428
});
427429

428430
if (exportJsonOnly) return result;
@@ -478,6 +480,7 @@ class SuperConverter {
478480
isFinalDoc = false,
479481
editor,
480482
isHeaderFooter = false,
483+
fieldsHighlightColor = null,
481484
}) {
482485
const bodyNode = this.savedTagsToRestore.find((el) => el.name === 'w:body');
483486

@@ -496,6 +499,7 @@ class SuperConverter {
496499
exportedCommentDefs: commentDefinitions,
497500
editor,
498501
isHeaderFooter,
502+
fieldsHighlightColor,
499503
});
500504

501505
return { result, params };

packages/super-editor/src/core/super-converter/exporter.js

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2009,7 +2009,7 @@ const translateFieldAttrsToMarks = (attrs = {}) => {
20092009
* @returns {XmlReadyNode} The translated field annotation node
20102010
*/
20112011
function translateFieldAnnotation(params) {
2012-
const { node, isFinalDoc } = params;
2012+
const { node, isFinalDoc, fieldsHighlightColor } = params;
20132013
const { attrs = {} } = node;
20142014
const annotationHandler = getTranslationByAnnotationType(attrs.type);
20152015
if (!annotationHandler) return {};
@@ -2031,7 +2031,14 @@ function translateFieldAnnotation(params) {
20312031
sdtContentElements = [...processedNode.elements];
20322032
}
20332033
}
2034-
sdtContentElements = [getFieldHighlightJson(), ...sdtContentElements];
2034+
2035+
sdtContentElements = [...sdtContentElements];
2036+
2037+
// Set field background color only if param is provided, default to transparent
2038+
const fieldBackgroundTag = getFieldHighlightJson(fieldsHighlightColor);
2039+
if (fieldBackgroundTag) {
2040+
sdtContentElements.unshift(fieldBackgroundTag);
2041+
}
20352042

20362043
// Contains only the main attributes.
20372044
const annotationAttrs = {
@@ -2478,14 +2485,37 @@ const getAutoPageJson = (type, outputMarks = []) => {
24782485
];
24792486
};
24802487

2481-
const getFieldHighlightJson = () => {
2488+
/**
2489+
* Get the JSON representation of the field highlight
2490+
* @param {string} fieldsHighlightColor - The highlight color for the field. Must be valid HEX.
2491+
* @returns {Object} The JSON representation of the field highlight
2492+
*/
2493+
export const getFieldHighlightJson = (fieldsHighlightColor) => {
2494+
if (!fieldsHighlightColor) return null;
2495+
2496+
// Normalize input
2497+
let parsedColor = fieldsHighlightColor.trim();
2498+
2499+
// Regex: optional '#' + 3/4/6/8 hex digits
2500+
const hexRegex = /^#?([A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/;
2501+
2502+
if (!hexRegex.test(parsedColor)) {
2503+
console.warn(`Invalid HEX color provided to fieldsHighlightColor export param: ${fieldsHighlightColor}`);
2504+
return null;
2505+
}
2506+
2507+
// Remove '#' if present
2508+
if (parsedColor.startsWith('#')) {
2509+
parsedColor = parsedColor.slice(1);
2510+
}
2511+
24822512
return {
24832513
name: 'w:rPr',
24842514
elements: [
24852515
{
24862516
name: 'w:shd',
24872517
attributes: {
2488-
'w:fill': '7AA6FF',
2518+
'w:fill': `#${parsedColor}`,
24892519
'w:color': 'auto',
24902520
'w:val': 'clear',
24912521
},
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { getFieldHighlightJson } from './exporter.js';
3+
4+
const extractFill = (result) => result?.elements?.[0]?.attributes?.['w:fill'];
5+
6+
describe('getFieldHighlightJson (non-throwing)', () => {
7+
let warnSpy;
8+
9+
beforeEach(() => {
10+
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
11+
});
12+
13+
afterEach(() => {
14+
warnSpy.mockRestore();
15+
});
16+
17+
it('returns null for falsy inputs (undefined, null, empty string)', () => {
18+
expect(getFieldHighlightJson()).toBeNull();
19+
expect(getFieldHighlightJson(undefined)).toBeNull();
20+
expect(getFieldHighlightJson(null)).toBeNull();
21+
expect(getFieldHighlightJson('')).toBeNull();
22+
expect(warnSpy).not.toHaveBeenCalled();
23+
});
24+
25+
it('accepts 3/4/6/8-digit HEX with or without # (case preserved)', () => {
26+
const cases = [
27+
['#FFF', '#FFF'],
28+
['FFF', '#FFF'],
29+
['#ffff', '#ffff'],
30+
['FFFF', '#FFFF'],
31+
['#A1B2C3', '#A1B2C3'],
32+
['a1b2c3', '#a1b2c3'],
33+
['#A1B2C3D4', '#A1B2C3D4'],
34+
['a1b2c3d4', '#a1b2c3d4'],
35+
];
36+
37+
for (const [input, expectedFill] of cases) {
38+
const out = getFieldHighlightJson(input);
39+
expect(out).toBeTruthy();
40+
expect(out.name).toBe('w:rPr');
41+
expect(extractFill(out)).toBe(expectedFill);
42+
expect(out.elements[0].attributes['w:color']).toBe('auto');
43+
expect(out.elements[0].attributes['w:val']).toBe('clear');
44+
}
45+
expect(warnSpy).not.toHaveBeenCalled();
46+
});
47+
48+
it('trims surrounding whitespace and still validates', () => {
49+
const out1 = getFieldHighlightJson(' #ABCDEF ');
50+
expect(extractFill(out1)).toBe('#ABCDEF');
51+
52+
const out2 = getFieldHighlightJson(' abc ');
53+
expect(extractFill(out2)).toBe('#abc');
54+
expect(warnSpy).not.toHaveBeenCalled();
55+
});
56+
57+
it('treats pure whitespace as invalid (returns null and warns)', () => {
58+
const out = getFieldHighlightJson(' ');
59+
expect(out).toBeNull();
60+
expect(warnSpy).toHaveBeenCalledTimes(1);
61+
expect(warnSpy.mock.calls[0][0]).toMatch(/Invalid HEX color/i);
62+
});
63+
64+
it('returns null and warns for invalid HEX formats', () => {
65+
const invalid = [
66+
'#GGG',
67+
'GGGZ',
68+
'red',
69+
'12345',
70+
'#12345',
71+
'#1234567',
72+
'1234567',
73+
'#',
74+
'##123',
75+
'xyz',
76+
'#ffffgfff',
77+
'12',
78+
'#12',
79+
];
80+
81+
for (const input of invalid) {
82+
const out = getFieldHighlightJson(input);
83+
expect(out).toBeNull();
84+
}
85+
expect(warnSpy).toHaveBeenCalledTimes(invalid.length);
86+
for (let i = 0; i < invalid.length; i++) {
87+
expect(warnSpy.mock.calls[i][0]).toMatch(/Invalid HEX color/i);
88+
}
89+
});
90+
91+
it('adds a leading # when missing', () => {
92+
const out = getFieldHighlightJson('ABCDEF');
93+
expect(extractFill(out)).toBe('#ABCDEF');
94+
expect(warnSpy).not.toHaveBeenCalled();
95+
});
96+
});

packages/superdoc/src/core/SuperDoc.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -742,10 +742,11 @@ export class SuperDoc extends EventEmitter {
742742
additionalFileNames = [],
743743
isFinalDoc = false,
744744
triggerDownload = true,
745+
fieldsHighlightColor = null,
745746
} = {}) {
746747
// Get the docx files first
747748
const baseFileName = exportedName ? cleanName(exportedName) : cleanName(this.config.title);
748-
const docxFiles = await this.exportEditorsToDOCX({ commentsType, isFinalDoc });
749+
const docxFiles = await this.exportEditorsToDOCX({ commentsType, isFinalDoc, fieldsHighlightColor });
749750
const blobsToZip = [...additionalFiles];
750751
const filenames = [...additionalFileNames];
751752

@@ -780,7 +781,7 @@ export class SuperDoc extends EventEmitter {
780781
* @param {{ commentsType?: string, isFinalDoc?: boolean }} [options]
781782
* @returns {Promise<Array<Blob>>}
782783
*/
783-
async exportEditorsToDOCX({ commentsType, isFinalDoc } = {}) {
784+
async exportEditorsToDOCX({ commentsType, isFinalDoc, fieldsHighlightColor } = {}) {
784785
const comments = [];
785786
if (commentsType !== 'clean') {
786787
if (this.commentsStore && typeof this.commentsStore.translateCommentsForExport === 'function') {
@@ -792,7 +793,7 @@ export class SuperDoc extends EventEmitter {
792793
this.superdocStore.documents.forEach((doc) => {
793794
const editor = doc.getEditor();
794795
if (editor) {
795-
docxPromises.push(editor.exportDocx({ isFinalDoc, comments, commentsType }));
796+
docxPromises.push(editor.exportDocx({ isFinalDoc, comments, commentsType, fieldsHighlightColor }));
796797
}
797798
});
798799
return await Promise.all(docxPromises);

0 commit comments

Comments
 (0)