@megaads/wm
Version:
To install the library, use npm:
1,165 lines (1,015 loc) • 70.7 kB
text/typescript
// @ts-nocheck
import {
CalendarElement,
CircularTextInputElement,
CustomizationOptions,
DatePickerOption,
DropdownOption,
DropdownValue,
DynamicImageElement,
ImagePlaceHolderElement,
ImageUploadOption,
SwatchOption,
SwatchValue,
Template,
TextInputElement,
TextInputOption,
VectorElement
} from "./types";
import axios from 'axios';
import {
getRenderConfigImage,
getRenderConfigTextBox,
getRenderConfigTextBoxCircular,
getRenderConfigVectorEps,
isNumeric,
} from "./helper";
import {ImageOptions} from "../images/types";
import Images from "../images";
type IOption = SwatchOption | DropdownOption | ImageUploadOption | TextInputOption | DatePickerOption;
type IElement = DynamicImageElement | TextInputElement | CircularTextInputElement | ImagePlaceHolderElement | VectorElement | CalendarElement;
class Customization {
private template: Template;
private options: IOption[];
private readonly breadcrumbs: {
id: number;
name: string;
slug: string,
_lft: number,
_rgt: number,
url: string
}[];
private readonly isAllowReplaceBackground: boolean;
private readonly baseUrl: string;
private LibCaching = {} as { [key: string]: string };
private groupId: string | number | undefined = undefined;
private groupIds: (string | number)[] = [];
private showElementIds: (string | number)[] = [];
private hideElementIds: (string | number)[] = [];
private selectedGroups = [] as { id: string; uuid: string, position: number }[];
private colorGuideImageUrl: string = '';
private spotifyToken: {access_token: string, expires_in: number} = { access_token: '', expires_in: 0 };
private searchSongResult = [];
private inputSearchSong = '';
private readonly isShowExcessiveOption = false;
// @ts-ignore
private groups: any;
constructor(customizationOptions: CustomizationOptions) {
this.template = customizationOptions.template;
this.options = customizationOptions.options;
this.groups = JSON.parse(this.template?.groups || '{}');
this.baseUrl = customizationOptions.baseUrl;
this.breadcrumbs = customizationOptions.breadcrumbs;
this.isAllowReplaceBackground = customizationOptions.isAllowReplaceBackground;
this.isShowExcessiveOption = !!customizationOptions.isShowExcessiveOption;
this.buildOptionSet();
this.options.forEach((option) => {
if (option.showSpotifyPlayer) {
option.showSpotifyPlayer = false;
}
if (option.isShow) {
if (option.type == 'swatch' || option.type == 'dropdown') {
this.changeSwatchOrDropdownValue(option as SwatchOption | DropdownOption);
} else if (option.type == 'text-input') {
this.changeTextInputValue(option as TextInputOption);
} else if (option.type == 'image-upload') {
this.changeImageUploadValue(option as ImageUploadOption);
} else if (option.type == 'date-picker') {
this.changeDatePickerValue(option as DatePickerOption);
}
}
});
}
getData() {
return {
template: this.template,
options: this.options,
}
}
async fetchImageUrlsFirstTime(): Promise<void> {
if (!this.template) {
return;
}
// fetch customily library
await this.fetchCustomilyLibrary()
for (const element of this.template.elements) {
if ("images" in element.config && element.config.images.length > 0 && element.config.imageId && !element.config.libraryId) {
const image = element.config.images.find(item => item.order == element.config.imageId);
if (image) {
element.config.imageUrl = image.value;
}
}
if ("fonts" in element.config && element.config.fonts.length > 0 && element.config.fontId) {
const font = element.config.fonts.find(item => item.order == element.config.fontId);
if (font) {
element.config.textConfig.font = font.value;
}
}
if ("colors" in element.config && element.config.colors.length > 0 && element.config.colorId) {
const color = element.config.colors.find(item => item.order == element.config.colorId);
if (color) {
element.config.textConfig.fill = color.value;
}
}
if ("vectors" in element.config && element.config.vectors.length > 0 && element.config.vectorId && !element.config.libraryId) {
const vector = element.config.vectors.find(item => item.order == element.config.vectorId);
if (vector) {
element.config.imageUrl = vector.value;
}
}
}
this.updateVisibleElement();
}
getTemplate(): Template {
return this.template;
}
getOptions(): IOption[] {
return this.options;
}
getElements(): IElement[] {
return this.template?.elements;
}
setTemplate(template: Template): void {
this.template = template;
}
setOptions(options: IOption[]): void {
this.options = options;
}
async selectOptionValue(option: IOption, value: SwatchValue | DropdownValue | null): Promise<void> {
this.buildOptionSet();
if (!this.template) {
return;
}
if (this.isChangeTemplateOption(option as SwatchOption | DropdownOption)) {
// fetch and set new template
// @ts-ignore
const templateId = value.templateId;
if (templateId) {
const newTemplate = await axios.get(`${this.baseUrl}/template/${templateId}`);
const templateData = newTemplate.data;
if (templateData) {
this.template = templateData.result;
this.groups = JSON.parse(this.template?.groups || '{}');
} else {
throw new Error('Template not found');
}
}
}
const childOptionIds = option.childOptionIds;
if (childOptionIds && childOptionIds.length) {
if (option.type == 'swatch' || option.type == 'dropdown') {
for (let i = 0; i < childOptionIds.length; i++) {
if (childOptionIds[i] == option.id) continue;
const childOptionIndex = this.options.findIndex(function (o) {
return o.id == childOptionIds[i];
});
if (childOptionIndex == -1) continue;
const childValues = option.childOptionIdValues[childOptionIds[i]];
const optionValues = option.childOptionIdValues[option.id];
const valueIndex = optionValues.findIndex(function (v: any) {
return v == value.value;
});
let childValue;
if (this.options[childOptionIndex].type == 'swatch') {
childValue = this.options[childOptionIndex].swatchValues.find(function (v: any) {
return v.value == childValues[valueIndex];
});
} else if (this.options[childOptionIndex].type == 'dropdown') {
childValue = this.options[childOptionIndex].dropdownValues.find(function (v: any) {
return v.value == childValues[valueIndex];
});
}
if (childValue) {
this.options[childOptionIndex].currentValue = childValue.id;
}
}
} else if (option.type == 'text-input') {
for (let i = 0; i < childOptionIds.length; i++) {
const childOptionIndex = this.options.findIndex(o => o.id == option.childOptionIds[i] && o.id != option.id);
if (childOptionIndex > -1) {
const maxLength = parseInt(this.options[childOptionIndex].config.max_length);
this.options[childOptionIndex].currentValue = option.currentValue.substr(0, maxLength);
}
}
}
this.buildOptionSet();
}
this.options.forEach((option) => {
if (option.isShow) {
if (option.type == 'swatch' || option.type == 'dropdown') {
this.changeSwatchOrDropdownValue(option as SwatchOption | DropdownOption);
} else if (option.type == 'text-input') {
this.changeTextInputValue(option as TextInputOption);
} else if (option.type == 'image-upload') {
this.changeImageUploadValue(option as ImageUploadOption);
} else if (option.type == 'date-picker') {
this.changeDatePickerValue(option as DatePickerOption);
}
}
});
// fetch customily library
await this.fetchCustomilyLibrary();
for (const element of this.template.elements) {
if ("images" in element.config && element.config.images.length > 0 && element.config.imageId && !element.config.libraryId) {
const image = element.config.images.find(item => item.order == element.config.imageId);
if (image) {
element.config.imageUrl = image.value;
}
}
if ("fonts" in element.config && element.config.fonts.length > 0 && element.config.fontId && !element.config.fontLibraryId) {
const font = element.config.fonts.find(item => item.order == element.config.fontId);
if (font) {
element.config.textConfig.font = font.value;
}
}
if ("colors" in element.config && element.config.colors.length > 0 && element.config.colorId && !element.config.colorLibraryId) {
const color = element.config.colors.find(item => item.order == element.config.colorId);
if (color) {
element.config.textConfig.fill = color.value;
}
}
if ("vectors" in element.config && element.config.vectors.length > 0 && element.config.vectorId && !element.config.libraryId) {
const vector = element.config.vectors.find(item => item.order == element.config.vectorId);
if (vector) {
element.config.imageUrl = vector.value;
}
}
}
this.updateVisibleElement();
}
private async fetchCustomilyLibrary() {
if (!this.template) {
return;
}
let elements = this.template.elements;
let promises = [];
for (const element of elements) {
if ("images" in element.config && !("library" in element.config) && element.config.imageId && element.config.libraryId) {
const libraryId = element.config.libraryId;
const position = element.config.imageId;
const libCacheKey = `${libraryId}-${position}`;
const endpoint = `https://app.customily.com/api/Libraries/${libraryId}/Elements/Position/${position}`;
const promise = new Promise<void>((resolve) => {
if (this.LibCaching[libCacheKey]) {
element.config.imageUrl = this.LibCaching[libCacheKey];
return resolve();
}
axios.get(endpoint).then((response) => {
const baseURL = "https://cdn.customily.com";
let pathProperty = 'Path';
if (response.data && typeof response.data[pathProperty] != 'undefined' && response.data[pathProperty]) {
let contentReplace = response.data[pathProperty].startsWith('/') ? '/Content' : 'Content';
this.LibCaching[libCacheKey] = `${baseURL}${response.data[pathProperty].replace(contentReplace, '')}`;
element.config.imageUrl = this.LibCaching[libCacheKey];
return resolve();
} else {
element.config.imageUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
}
return resolve();
})
});
promises.push(promise);
}
if ("colors" in element.config && !("color_library" in element.config) && element.config.colorId && element.config.colorLibraryId) {
const libraryId = element.config.colorLibraryId;
const position = element.config.colorId;
const libCacheKey = `${libraryId}-${position}`;
const endpoint = `https://app.customily.com/api/Libraries/${libraryId}/Elements/Position/${position}`;
const promise = new Promise<void>((resolve) => {
if (this.LibCaching[libCacheKey]) {
if ("textConfig" in element.config) {
element.config.textConfig.fill = this.LibCaching[libCacheKey];
}
return resolve();
}
axios.get(endpoint).then((response) => {
let pathProperty = 'hex';
if (response.data && typeof response.data[pathProperty] != 'undefined' && response.data[pathProperty]) {
this.LibCaching[libCacheKey] = response.data[pathProperty];
if ("textConfig" in element.config) {
element.config.textConfig.fill = this.LibCaching[libCacheKey];
}
return resolve();
}
return resolve();
});
});
promises.push(promise);
}
if ("fonts" in element.config && !("font_library" in element.config) && element.config.fontId && element.config.fontLibraryId) {
const libraryId = element.config.fontLibraryId;
const position = element.config.fontId;
const libCacheKey = `${libraryId}-${position}`;
const endpoint = `https://app.customily.com/api/Libraries/${libraryId}/Elements/Position/${position}`;
const promise = new Promise<void>((resolve) => {
if (this.LibCaching[libCacheKey]) {
if ("textConfig" in element.config) {
element.config.textConfig.font = this.LibCaching[libCacheKey];
}
return resolve();
}
axios.get(endpoint).then((response) => {
const baseURL = "https://cdn.customily.com";
let pathProperty = 'Path';
if (response.data && typeof response.data[pathProperty] != 'undefined' && response.data[pathProperty]) {
let contentReplace = response.data[pathProperty].startsWith('/') ? '/Content' : 'Content';
this.LibCaching[libCacheKey] = `${baseURL}${response.data[pathProperty].replace(contentReplace, '')}`;
if ("textConfig" in element.config) {
element.config.textConfig.font = this.LibCaching[libCacheKey];
element.config.textConfig.fontFamily = 'customFont-' + response.data.fontId;
element.config.textConfig.fontFamilyDownload = this.LibCaching[libCacheKey];
}
return resolve();
}
return resolve();
});
});
promises.push(promise);
}
if ("vectors" in element.config && !("vector_library" in element.config) && element.config.vectorId && element.config.libraryId) {
const libraryId = element.config.libraryId;
const position = element.config.vectorId;
const libCacheKey = `${libraryId}-${position}`;
const endpoint = `https://app.customily.com/api/Libraries/${libraryId}/Elements/Position/${position}`;
const promise = new Promise<void>((resolve) => {
if (this.LibCaching[libCacheKey]) {
element.config.imageUrl = this.LibCaching[libCacheKey];
return resolve();
}
axios.get(endpoint).then((response) => {
const baseURL = "https://cdn.customily.com";
let pathProperty = 'svgPath';
if (response.data && typeof response.data[pathProperty] != 'undefined' && response.data[pathProperty]) {
let contentReplace = response.data[pathProperty].startsWith('/') ? '/Content' : 'Content';
this.LibCaching[libCacheKey] = `${baseURL}${response.data[pathProperty].replace(contentReplace, '')}`;
element.config.imageUrl = this.LibCaching[libCacheKey];
return resolve();
} else {
element.config.imageUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
}
return resolve();
});
});
promises.push(promise);
}
}
await Promise.all(promises);
}
private buildOptionSet() {
// set currentValue for selected value
this.options.forEach((option: IOption) => {
if (option.type == 'swatch' || option.type == 'dropdown') {
let selectedValue;
if ("dropdownValues" in option) {
selectedValue = option.dropdownValues?.find((value: DropdownValue) => value.selected);
} else if ("swatchValues" in option) {
selectedValue = option.swatchValues?.find((value: SwatchValue) => value.selected);
}
// @ts-ignore
if (selectedValue && !Object.hasOwnProperty.call(option, 'currentValue')) {
// @ts-ignore
option.currentValue = isNaN(selectedValue.id) ? selectedValue.valueName : selectedValue.id;
}
if (selectedValue && Object.hasOwnProperty.call(selectedValue, 'groupId')) {
this.groupId = selectedValue.groupId;
}
}
});
// build isShow for option
this.options.forEach((option: IOption) => {
option.isShow = this.isShowOption(this.options, option);
if (option.isShow) {
if (this.isExcessiveSizeOption(option) || this.isExcessiveColorOption(option)) {
option.hideVisually = true;
}
}
// hide option has label --------------
if (/^-{5,}$/.test(option.label)) {
option.hideVisually = true;
}
});
// re-check isShow option
this.options.forEach((optionItem: IOption) => {
if (optionItem.isShow) {
optionItem.isShow = this.isShowOption(this.options, optionItem);
}
});
}
private findOption(watchOptionId: number) {
let watchOption = this.options.find(item => item.id == watchOptionId);
if (!watchOption) {
watchOption = this.options.find(item => item.id == this.groupId);
}
return watchOption;
}
private getConditionWatchOptionId(condition: any) {
if (typeof condition.watchOptionId != 'undefined') {
return condition.watchOptionId;
}
return condition.watch_option_id;
}
private getConditionDesiredValue(condition: any) {
if (typeof condition.desiredValue != 'undefined') {
return condition.desiredValue;
}
return condition.desired_value;
}
private getConditionCombinationOperator(condition: any) {
return condition.combinationOperator || condition.combination_operator || 'and';
}
private getConditionAction(condition: any) {
return (condition.action || 'show').toLowerCase();
}
private getOptionConditions(option: any) {
const variationConditions = option.variationConditions;
if (variationConditions && variationConditions.length) {
return variationConditions;
}
return option.conditions || [];
}
/**
* Resolve the value object an option is currently set to. Handles
* both encodings of `currentValue` — numeric `id` (the common case
* set by `buildOptionSet` and most host callers) and the
* `valueName` fallback used when the value's id is non-numeric.
*
* Used by the condition evaluators below so a desiredValue
* encoded as either the value's `id`, `valueName`, or `value`
* matches reliably — Customily-authored conditions historically
* mix all three encodings.
*/
private resolveSelectedValue(option: IOption): SwatchValue | DropdownValue | null {
const values: (SwatchValue | DropdownValue)[] =
("dropdownValues" in option && Array.isArray(option.dropdownValues) && option.dropdownValues) ||
("swatchValues" in option && Array.isArray(option.swatchValues) && option.swatchValues) ||
[];
if (!values.length) return null;
// @ts-ignore — IOption union doesn't expose currentValue uniformly
const cv = option.currentValue;
return (
values.find((v: any) => v && v.id === cv) ??
values.find((v: any) => v && v.id == cv) ??
values.find((v: any) => v && v.valueName === cv) ??
values.find((v: any) => v && v.selected) ??
null
);
}
/**
* Equality check for `desiredValue`. Customily's canonical convention
* is that a condition's `desiredValue` matches the watched option's
* selected value's `id`. When the option's values carry an `id`, the
* matcher must restrict comparison to that field — falling through to
* `value` / `valueName` on the same value object false-positives
* whenever the `id` and `value` ranges overlap inside one option:
*
* {id: 2, value: 0, valueName: "The Man"} // ← user picked this
* {id: 0, value: 1, valueName: "Best Dad"} // ← condition desired=[1]
*
* Without the restriction, Color sibling-options light up the wrong
* branch ("Best Dad" picked → "Awesome Dad" image renders) because
* `desired === selected.value` (1 === 1) hits even though the author
* meant `desired === selected.id` (1 ≠ 0). Only when the value shape
* doesn't ship an `id` (older Customily Color dropdowns expose just
* `value` + `valueName`) do we fall back to those fields.
*/
private matchesDesiredValue(watchOption: IOption, desiredValue: unknown): boolean {
if (!watchOption) return false;
// @ts-ignore
const cv = watchOption.currentValue;
const desired = Array.isArray(desiredValue) ? desiredValue : [desiredValue];
// The "-1" wildcard means "any non-empty selection". Preserve
// the original semantics — only fires when something is set.
if (desired.includes(-1) && cv !== undefined && cv !== '') return true;
const selected = this.resolveSelectedValue(watchOption);
if (selected) {
// @ts-ignore
const hasId = selected.id !== undefined && selected.id !== null;
if (hasId) {
// id-only match — strict first, then loose for cross-type
// tolerance (JSON sometimes ships `1` and `"1"` for the
// same logical id).
for (const d of desired) {
// @ts-ignore
if (d === selected.id) return true;
// @ts-ignore
if (d == selected.id) return true;
}
return false;
}
// No `id` → match by `value` then `valueName`. Same dual
// strict/loose layout for type tolerance.
for (const d of desired) {
// @ts-ignore
if (selected.value !== undefined && d === selected.value) return true;
// @ts-ignore
if (selected.value !== undefined && d == selected.value) return true;
// @ts-ignore
if (d === selected.valueName) return true;
}
return false;
}
// No resolvable value list (text-input / image-upload). Compare
// directly against the option's raw `currentValue`.
for (const d of desired) {
// @ts-ignore
if (d == cv) return true;
}
return false;
}
private getConditionValuesForOption(options: IOption[], conditions: any[]) {
let conditionValues = [];
for (const condition of conditions) {
const watchOptionId = this.getConditionWatchOptionId(condition);
if (typeof watchOptionId != 'undefined') {
let watchOption = options.find(item => item.id == watchOptionId);
const action = this.getConditionAction(condition);
const desiredValue = this.getConditionDesiredValue(condition);
if (watchOption && watchOption.isShow) {
if (this.matchesDesiredValue(watchOption, desiredValue)) {
conditionValues.push({
value: action === 'hide' ? false : true,
combinationOperator: this.getConditionCombinationOperator(condition),
});
} else {
conditionValues.push({
value: action === 'hide' ? true : false,
combinationOperator: this.getConditionCombinationOperator(condition),
})
}
} else {
// if watchOptionId not found
// skip if conditions length > 1
if (conditions.length > 1 && !watchOption) {
continue;
}
let value = true;
if (action !== 'hide' && watchOption) {
value = false;
}
conditionValues.push({
value: value,
combinationOperator: this.getConditionCombinationOperator(condition),
});
}
}
}
return conditionValues;
}
private evaluateConditionList(options: IOption[], conditions: any[]) {
if (conditions && conditions.length > 0) {
return this.reduceConditionValues(this.getConditionValuesForOption(options, conditions));
}
return true;
}
private evaluateGroupConditions(option: IOption) {
if (option.groupConditions && option.groupConditions.length) {
for (const groupConditions of option.groupConditions) {
let conditionsByGroup = [];
let retValByGroup = false;
for (let i = 0; i < groupConditions.length; i++) {
const condition = this.getConditionValue(groupConditions[i]);
if (condition) {
conditionsByGroup.push(condition);
}
}
// combination
let finalCondition = true;
if (conditionsByGroup.length) {
finalCondition = conditionsByGroup[0].value;
for (let i = 1; i < conditionsByGroup.length; i++) {
const condition = conditionsByGroup[i];
if (condition.operator === 'or') {
finalCondition = finalCondition || condition.value;
} else {
finalCondition = finalCondition && condition.value;
}
}
}
retValByGroup = finalCondition;
if (retValByGroup) { // pass if any group condition is true
return true;
}
}
return false;
}
return true;
}
private evaluateOptionConditions(options: IOption[], option: IOption) {
const baseConditions = option.groupConditions && option.groupConditions.length
? this.evaluateGroupConditions(option)
: this.evaluateConditionList(options, option.conditions || []);
const variationConditions = this.evaluateConditionList(options, option.variationConditions || []);
return baseConditions && variationConditions;
}
private reduceConditionValues(conditionValues: { value: boolean; combinationOperator: string }[]) {
if (!conditionValues.length) {
return true;
}
let finalCondition = conditionValues[0].value;
for (let i = 1; i < conditionValues.length; i++) {
const condition = conditionValues[i];
if (condition.combinationOperator === 'or') {
finalCondition = finalCondition || condition.value;
} else {
finalCondition = finalCondition && condition.value;
}
}
return finalCondition;
}
private getConditionValue(condition: any) {
const desiredValue = this.getConditionDesiredValue(condition);
const watchOption = this.findOption(this.getConditionWatchOptionId(condition));
if (typeof watchOption != 'undefined' && watchOption) {
// Delegate to the shared matcher so groupConditions resolve
// identically to regular conditions — historically this
// branch read `watchOption.value` / `.valueName`, fields the
// SDK never populates on options, so non-numeric desired
// values silently never matched.
let conditionValue = this.matchesDesiredValue(watchOption, desiredValue);
if (this.getConditionAction(condition) === 'hide') {
conditionValue = !conditionValue;
}
if (!watchOption.isShow) {
conditionValue = this.getConditionAction(condition) === 'hide';
}
return {
value: conditionValue,
operator: this.getConditionCombinationOperator(condition)
};
}
return null;
}
private isShowOption(options: IOption[], option: IOption): boolean {
return this.evaluateOptionConditions(options, option);
}
private isExcessiveSizeOption(obj: IOption) {
if (this.isShowExcessiveOption) {
return false;
}
const isLabelSize = obj.label && obj.label.toLowerCase().includes('size');
const sizeValues = ['S', 'M', 'L', 'XL', '2XL', '3XL', '4XL'];
let valuesToCheck;
if ('dropdownValues' in obj) {
valuesToCheck = obj.dropdownValues;
} else if ('swatchValues' in obj) {
valuesToCheck = obj.swatchValues;
}
const areValuesSizes = valuesToCheck && valuesToCheck.some(value =>
sizeValues.includes(value.valueName)
);
return isLabelSize && areValuesSizes;
}
private isExcessiveColorOption(obj: IOption) {
if (this.isShowExcessiveOption) {
return false;
}
const isLabelColor = obj.label && obj.label.toLowerCase().includes('color');
const validateLabelColors = [
'Shirt\'s Color', 'Hoodie\'s Color', 'Hoodie Color',
'Sweatshirt\'s Color', 'Sweatshirt Color', 'Tank Top\'s Color', 'Tank Top Color',
'T-Shirt\'s Color', 'T-Shirt Color', 'Polo\'s Color', 'Polo Color', 'Tee Color'
]
let isTShirt = false;
if (this.breadcrumbs && this.breadcrumbs.length > 2 && this.breadcrumbs[2].id == 7) {
isTShirt = true;
}
let isValidLabelColor: boolean;
if (isTShirt) {
isValidLabelColor = validateLabelColors.some(label => obj.label.toLowerCase().includes(label.toLowerCase()));
} else {
isValidLabelColor = validateLabelColors.includes(obj.label);
}
return isLabelColor && isValidLabelColor;
}
private changeSwatchOrDropdownValue(option: SwatchOption | DropdownOption): void {
if (!this.template) {
return;
}
let values;
if ("dropdownValues" in option) {
values = option.dropdownValues;
}
if ("swatchValues" in option) {
values = option.swatchValues;
}
if (!values) {
return;
}
// Resolve which value the option is currently set to. Customily ships
// values that may not carry an `id` (Color dropdowns we've seen only
// expose `imageId`, `value`, `valueName`, `order`), so a strict
// `item.id === currentValue` miss falls back to the still-`selected`
// default — leaving `element.config.imageId` pinned to the original
// image and silently hitting the library cache. Match against every
// field the host might have stored in `currentValue`, with loose
// equality so `"5"` and `5` interchange, and only fall back to the
// selected flag when no field matches.
const cv = option.currentValue;
let value = values.find((item: SwatchValue | DropdownValue) => item.id !== undefined && item.id === cv);
// eslint-disable-next-line eqeqeq
if (!value) value = values.find((item: SwatchValue | DropdownValue) => item.id !== undefined && item.id == cv);
if (!value) value = values.find((item: SwatchValue | DropdownValue) => item.valueName !== undefined && item.valueName === cv);
// eslint-disable-next-line eqeqeq
if (!value) value = values.find((item: SwatchValue | DropdownValue) => item.value !== undefined && item.value == cv);
if (!value) value = values.find((item: SwatchValue | DropdownValue) => item.selected);
if (!value) {
return;
}
// Keep `selected` in sync so a follow-up call that loses
// `currentValue` still picks the right value via the fallback.
for (const v of values) {
v.selected = v === value;
}
if (value.groupId) {
this.groupId = value.groupId;
}
option.groupIds = values.map((v) => v.groupId);
option.groupIds = option.groupIds.filter(item => item !== undefined);
if (option.groupIds) {
// xóa group không kích hoạt
const needRemoveGroupIds = option.groupIds.filter((groupId) => {
return groupId != value.groupId;
});
if (this.groupIds && Array.isArray(this.groupIds)) {
this.groupIds = this.groupIds.filter(groupId => !needRemoveGroupIds.includes(groupId));
}
// thêm group cần kích hoạt
if (this.groupIds && !this.groupIds.includes(value.groupId)) {
this.groupIds.push(value.groupId);
}
this.groupIds = this.groupIds.filter(item => item !== undefined);
}
let functionItems = option.functionItems;
if (!functionItems) {
return;
}
for (let functionItem of functionItems) {
let elementId = functionItem.elementId;
// find element in template
let element = this.template.elements.find(
item => item.elementId == elementId
);
if (!element) {
continue;
}
if (functionItem.type == 'dynamic-image') {
let valueId = this.getValueId(value, option);
let dynamicImageElement: DynamicImageElement = element as DynamicImageElement;
if (dynamicImageElement.config.images?.length && ("library" in dynamicImageElement.config)) {
let image = dynamicImageElement.config.images?.find(
item => item.order == valueId
);
if (image) {
dynamicImageElement.config.imageUrl = image.value;
dynamicImageElement.config.imageId = valueId;
} else {
dynamicImageElement.config.imageUrl = '';
dynamicImageElement.config.imageId = valueId;
}
} else {
dynamicImageElement.config.imageId = valueId;
if (dynamicImageElement.config.images?.length) {
let image = dynamicImageElement.config.images?.find(
item => item.order == valueId
);
if (image) {
dynamicImageElement.config.imageUrl = image.value;
} else {
dynamicImageElement.config.imageUrl = '';
}
}
}
// check if element has child elements
if (dynamicImageElement.childElementIds?.length) {
for (let childElementId of dynamicImageElement.childElementIds) {
let childElement = this.template.elements.find(
item => item.elementId == childElementId
) as DynamicImageElement;
if (childElement) {
childElement.config.imageId = valueId;
childElement.config.imageUrl = dynamicImageElement.config.imageUrl;
this.setElement(childElement, childElementId);
}
}
}
this.setElement(dynamicImageElement, elementId);
} else if (functionItem.type == 'font-type') {
let valueId = this.getValueId(value, option);
let textInputElement = element as TextInputElement;
if (textInputElement.config.fonts) {
let font = textInputElement.config.fonts.find(
item => item.order == valueId
);
if (font) {
textInputElement.config.textConfig.font = font.value;
textInputElement.config.fontId = valueId;
}
} else {
textInputElement.config.fontId = valueId;
}
// check if element has child elements
if (textInputElement.childElementIds?.length) {
for (let childElementId of textInputElement.childElementIds) {
let childElement = this.template.elements.find(
item => item.elementId == childElementId
) as TextInputElement;
if (childElement) {
childElement.config.fontId = valueId;
if (textInputElement.config.textConfig.font) {
childElement.config.textConfig.font = textInputElement.config.textConfig.font;
this.setElement(childElement, childElementId)
}
}
}
}
this.setElement(textInputElement, elementId);
} else if (functionItem.type == 'text-color') {
let valueId = this.getValueId(value, option);
let textInputElement = element as TextInputElement;
if (textInputElement.config.colors) {
let color = textInputElement.config.colors.find(
item => item.order == valueId
);
if (color) {
textInputElement.config.textConfig.fill = color.value;
textInputElement.config.colorId = valueId;
}
} else {
textInputElement.config.colorId = valueId;
}
// check if element has child elements
if (textInputElement.childElementIds?.length) {
for (let childElementId of textInputElement.childElementIds) {
let childElement = this.template.elements.find(
item => item.elementId == childElementId
) as TextInputElement;
if (childElement) {
if (textInputElement.config.textConfig.fill) {
childElement.config.textConfig.fill = textInputElement.config.textConfig.fill
}
childElement.config.colorId = valueId;
this.setElement(childElement, childElementId)
}
}
}
this.setElement(textInputElement, elementId);
} else if (functionItem.type === 'vector') {
let valueId = this.getValueId(value, option);
let vectorElement: VectorElement = element as VectorElement;
if (vectorElement.config.vectors?.length) {
let image = vectorElement.config.vectors?.find(
item => item.order == valueId
);
if (image) {
vectorElement.config.imageUrl = image.value;
vectorElement.config.vectorId = valueId;
}
} else {
vectorElement.config.vectorId = valueId;
}
// check if element has child elements
if (vectorElement.childElementIds?.length) {
for (let childElementId of vectorElement.childElementIds) {
let childElement = this.template.elements.find(
item => item.elementId == childElementId
) as DynamicImageElement;
if (childElement) {
childElement.config.imageId = valueId;
if (vectorElement.config.imageUrl) {
childElement.config.imageUrl = vectorElement.config.imageUrl;
this.setElement(childElement, childElementId);
}
}
}
}
this.setElement(element, elementId);
}
}
}
private changeImageUploadValue(option: ImageUploadOption) :void {
if (!this.template) {
return;
}
if (!option.fileUploadImageId || typeof option.currentValue === 'undefined') {
return;
}
const imageUrl = option.currentValue.toString();
let uploadImageElementId = option.fileUploadImageId;
let uploadImageElement = this.template.elements.find(
item => item.elementId == uploadImageElementId
) as DynamicImageElement | ImagePlaceHolderElement;
if (uploadImageElement) {
uploadImageElement.config.imageUrl = imageUrl;
// check if element has child elements
if (uploadImageElement.childElementIds?.length) {
for (let childElementId of uploadImageElement.childElementIds) {
let childElement = this.template.elements.find(
item => item.elementId == childElementId
) as DynamicImageElement | ImagePlaceHolderElement;
if (childElement) {
childElement.config.imageUrl = imageUrl;
// set element to template.elements
let index = this.template.elements.findIndex(
item => item.elementId == childElementId
);
if (index >= 0) {
this.template.elements[index] = childElement;
}
}
}
}
// set element to template.elements
let index = this.template.elements.findIndex(
item => item.elementId == uploadImageElementId
);
if (index >= 0) {
this.template.elements[index] = uploadImageElement;
}
}
}
/**
* Apply a Date Picker option's selection to every calendar element it
* targets via `functionItems` of type `calendar`. The selected date is
* parsed from the option's `currentValue` (ISO `YYYY-MM-DD`) and
* overlaid onto the element's `calendarConfig` as
* `selectedDate` / `selectedYear` / `selectedMonth` / `selectedDay` so a
* downstream renderer can mark the day on the rendered month grid.
*
* Pre-existing `calendarConfig` fields (markerType, fonts, colours,
* etc.) are preserved — only the selection keys are overwritten. When
* the option has no value, the selection keys are cleared so the grid
* renders without a marker.
*/
/**
* Normalize `calendarConfig` to a plain object. Customily ships it as a
* JSON-encoded string on some endpoints and as a parsed object on
* others. Spreading a string with `{...str}` produces a
* character-indexed dictionary (`{"0":"{","1":"\"","2":"m",...}`), so
* normalize here before we touch it. Falls back to `{}` when the value
* is missing, invalid JSON, or some unexpected primitive.
*/
private parseCalendarConfig(value: any): any {
if (!value) return {};
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value);
if (parsed && typeof parsed === 'object') return parsed;
} catch (e) {
// Fall through to empty object below.
}
return {};
}
if (typeof value === 'object') return value;
return {};
}
private changeDatePickerValue(option: DatePickerOption): void {
if (!this.template) {
return;
}
const functionItems = option.functionItems;
if (!functionItems || !functionItems.length) {
return;
}
const iso = option.currentValue != null ? String(option.currentValue) : '';
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
const selectedDate = m ? iso : '';
const selectedYear = m ? parseInt(m[1], 10) : null;
const selectedMonth = m ? parseInt(m[2], 10) : null;
const selectedDay = m ? parseInt(m[3], 10) : null;
for (const functionItem of functionItems) {
// Restrict to calendar function items. Other function types on a
// Date Picker option (rare, but possible if Customily ever
// attaches a secondary effect) shouldn't have their config
// clobbered with date-picker state.
if (functionItem.type && functionItem.type !== 'calendar') {
continue;
}
const elementId = functionItem.elementId;
if (elementId === undefined) {
continue;
}
const element = this.template.elements.find(
item => item.elementId == elementId
);
if (!element) {
continue;
}
const calendarElement = element as CalendarElement;
const existing = this.parseCalendarConfig(calendarElement.config.calendarConfig);
calendarElement.config.calendar = true;
calendarElement.config.calendarConfig = {
...existing,
selectedDate,
selectedYear,
selectedMonth,
selectedDay,
};
// Propagate to child elements so multi-page or grouped calendar
// setups (rare, but present in some Customily templates) stay in
// sync with the parent's selection.
if (calendarElement.childElementIds && calendarElement.childElementIds.length) {
for (const childElementId of calendarElement.childElementIds) {
const childElement = this.template.elements.find(
item => item.elementId == childElementId
) as CalendarElement | undefined;
if (childElement && childElement.config) {