Skip to content

Commit 2074ed0

Browse files
committed
fix: default field export background transparent, allow config on export
1 parent 6b6d511 commit 2074ed0

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
@@ -1542,6 +1542,7 @@ export class Editor extends EventEmitter {
15421542
exportXmlOnly = false,
15431543
comments = [],
15441544
getUpdatedDocs = false,
1545+
fieldsHighlightColor = null,
15451546
} = {}) {
15461547
// Pre-process the document state to prepare for export
15471548
const json = this.#prepareDocumentForExport(comments);
@@ -1556,6 +1557,7 @@ export class Editor extends EventEmitter {
15561557
comments,
15571558
this,
15581559
exportJsonOnly,
1560+
fieldsHighlightColor,
15591561
);
15601562

15611563
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
@@ -407,6 +407,7 @@ class SuperConverter {
407407
comments = [],
408408
editor,
409409
exportJsonOnly = false,
410+
fieldsHighlightColor,
410411
) {
411412
const commentsWithParaIds = comments.map((c) => prepareCommentParaIds(c));
412413
const commentDefinitions = commentsWithParaIds.map((c, index) =>
@@ -421,6 +422,7 @@ class SuperConverter {
421422
commentsExportType,
422423
isFinalDoc,
423424
editor,
425+
fieldsHighlightColor,
424426
});
425427

426428
if (exportJsonOnly) return result;
@@ -476,6 +478,7 @@ class SuperConverter {
476478
isFinalDoc = false,
477479
editor,
478480
isHeaderFooter = false,
481+
fieldsHighlightColor = null,
479482
}) {
480483
const bodyNode = this.savedTagsToRestore.find((el) => el.name === 'w:body');
481484

@@ -494,6 +497,7 @@ class SuperConverter {
494497
exportedCommentDefs: commentDefinitions,
495498
editor,
496499
isHeaderFooter,
500+
fieldsHighlightColor,
497501
});
498502

499503
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
@@ -2313,7 +2313,7 @@ const translateFieldAttrsToMarks = (attrs = {}) => {
23132313
* @returns {XmlReadyNode} The translated field annotation node
23142314
*/
23152315
function translateFieldAnnotation(params) {
2316-
const { node, isFinalDoc } = params;
2316+
const { node, isFinalDoc, fieldsHighlightColor } = params;
23172317
const { attrs = {} } = node;
23182318
const annotationHandler = getTranslationByAnnotationType(attrs.type);
23192319
if (!annotationHandler) return {};
@@ -2335,7 +2335,14 @@ function translateFieldAnnotation(params) {
23352335
sdtContentElements = [...processedNode.elements];
23362336
}
23372337
}
2338-
sdtContentElements = [getFieldHighlightJson(), ...sdtContentElements];
2338+
2339+
sdtContentElements = [...sdtContentElements];
2340+
2341+
// Set field background color only if param is provided, default to transparent
2342+
const fieldBackgroundTag = getFieldHighlightJson(fieldsHighlightColor);
2343+
if (fieldBackgroundTag) {
2344+
sdtContentElements.unshift(fieldBackgroundTag);
2345+
}
23392346

23402347
// Contains only the main attributes.
23412348
const annotationAttrs = {
@@ -2732,14 +2739,37 @@ const getAutoPageJson = (type, outputMarks = []) => {
27322739
];
27332740
};
27342741

2735-
const getFieldHighlightJson = () => {
2742+
/**
2743+
* Get the JSON representation of the field highlight
2744+
* @param {string} fieldsHighlightColor - The highlight color for the field. Must be valid HEX.
2745+
* @returns {Object} The JSON representation of the field highlight
2746+
*/
2747+
export const getFieldHighlightJson = (fieldsHighlightColor) => {
2748+
if (!fieldsHighlightColor) return null;
2749+
2750+
// Normalize input
2751+
let parsedColor = fieldsHighlightColor.trim();
2752+
2753+
// Regex: optional '#' + 3/4/6/8 hex digits
2754+
const hexRegex = /^#?([A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/;
2755+
2756+
if (!hexRegex.test(parsedColor)) {
2757+
console.warn(`Invalid HEX color provided to fieldsHighlightColor export param: ${fieldsHighlightColor}`);
2758+
return null;
2759+
}
2760+
2761+
// Remove '#' if present
2762+
if (parsedColor.startsWith('#')) {
2763+
parsedColor = parsedColor.slice(1);
2764+
}
2765+
27362766
return {
27372767
name: 'w:rPr',
27382768
elements: [
27392769
{
27402770
name: 'w:shd',
27412771
attributes: {
2742-
'w:fill': '7AA6FF',
2772+
'w:fill': `#${parsedColor}`,
27432773
'w:color': 'auto',
27442774
'w:val': 'clear',
27452775
},
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
comments.push(...this.commentsStore?.translateCommentsForExport());
@@ -790,7 +791,7 @@ export class SuperDoc extends EventEmitter {
790791
this.superdocStore.documents.forEach((doc) => {
791792
const editor = doc.getEditor();
792793
if (editor) {
793-
docxPromises.push(editor.exportDocx({ isFinalDoc, comments, commentsType }));
794+
docxPromises.push(editor.exportDocx({ isFinalDoc, comments, commentsType, fieldsHighlightColor }));
794795
}
795796
});
796797
return await Promise.all(docxPromises);

0 commit comments

Comments
 (0)