UNPKG

@megaads/wm

Version:

To install the library, use npm:

1,165 lines (1,015 loc) 70.7 kB
// @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) {