diff --git a/cspell.config.yaml b/cspell.config.yaml index c559e228..7a23ee4a 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -49,6 +49,7 @@ words: - Typecheck - uncategorized - unflatten + - unproxify - weba - webformat - xslx diff --git a/src/lib/components/contents/details/widgets/list/list-editor.svelte b/src/lib/components/contents/details/widgets/list/list-editor.svelte index aac9042e..df2f8d67 100644 --- a/src/lib/components/contents/details/widgets/list/list-editor.svelte +++ b/src/lib/components/contents/details/widgets/list/list-editor.svelte @@ -66,12 +66,14 @@ let mounted = false; let widgetId = ''; let inputValue = ''; - let itemExpandedList = []; onMount(() => { mounted = true; widgetId = generateUUID().split('-').pop(); - itemExpandedList = items.map(() => !collapsed); + + items.forEach((__, index) => { + $entryDraft.viewStates[locale][`${keyPath}.${index}.expanded`] = !collapsed; + }); }); /** @@ -116,8 +118,7 @@ /** * Update the value for the List widget with subfield(s). - * @param {(list: any[]) => void} manipulate A function to manipulate the list, which takes one - * argument of the list itself. The typical usage is `list.splice()`. + * @param {({ valueList, viewList }) => void} manipulate See {@link updateListField}. */ const updateComplexList = (manipulate) => { Object.keys($entryDraft.currentValues).forEach((_locale) => { @@ -133,7 +134,7 @@ * @see https://decapcms.org/docs/beta-features/#list-widget-variable-types */ const addItem = (subFieldName) => { - updateComplexList((list) => { + updateComplexList(({ valueList, viewList }) => { const newItem = {}; const subFields = subFieldName @@ -153,10 +154,10 @@ newItem[typeKey] = subFieldName; } - const index = addToTop ? 0 : list.length; + const index = addToTop ? 0 : valueList.length; - list.splice(index, 0, newItem); - itemExpandedList.splice(index, 0, true); + valueList.splice(index, 0, newItem); + viewList.splice(index, 0, { expanded: true }); }); }; @@ -165,9 +166,9 @@ * @param {number} index Target index. */ const deleteItem = (index) => { - updateComplexList((list) => { - list.splice(index, 1); - itemExpandedList.splice(index, 1); + updateComplexList(({ valueList, viewList }) => { + valueList.splice(index, 1); + viewList.splice(index, 1); }); }; @@ -176,12 +177,9 @@ * @param {number} index Target index. */ const moveUpItem = (index) => { - updateComplexList((list) => { - [list[index], list[index - 1]] = [list[index - 1], list[index]]; - [itemExpandedList[index], itemExpandedList[index - 1]] = [ - itemExpandedList[index - 1], - itemExpandedList[index], - ]; + updateComplexList(({ valueList, viewList }) => { + [valueList[index], valueList[index - 1]] = [valueList[index - 1], valueList[index]]; + [viewList[index], viewList[index - 1]] = [viewList[index - 1], viewList[index]]; }); }; @@ -190,12 +188,9 @@ * @param {number} index Target index. */ const moveDownItem = (index) => { - updateComplexList((list) => { - [list[index], list[index + 1]] = [list[index + 1], list[index]]; - [itemExpandedList[index], itemExpandedList[index + 1]] = [ - itemExpandedList[index + 1], - itemExpandedList[index], - ]; + updateComplexList(({ valueList, viewList }) => { + [valueList[index], valueList[index + 1]] = [valueList[index + 1], valueList[index]]; + [viewList[index], viewList[index + 1]] = [viewList[index + 1], viewList[index]]; }); }; @@ -234,21 +229,22 @@
{#each items as item, index} + {@const expanded = !!$entryDraft.viewStates[locale][`${keyPath}.${index}.expanded`]}
- {#if itemExpandedList[index]} + {#if expanded} {@const subFieldName = Array.isArray(types) ? $entryDraft.currentValues[locale][`${keyPath}.${index}.${typeKey}`] : undefined} diff --git a/src/lib/components/contents/details/widgets/select/select-editor.svelte b/src/lib/components/contents/details/widgets/select/select-editor.svelte index befda794..b4544b0c 100644 --- a/src/lib/components/contents/details/widgets/select/select-editor.svelte +++ b/src/lib/components/contents/details/widgets/select/select-editor.svelte @@ -43,8 +43,7 @@ /** * Update the value for the list. - * @param {(list: any[]) => void} manipulate A function to manipulate the list, which takes one - * argument of the list itself. The typical usage is `list.splice()`. + * @param {({ valueList, viewList }) => void} manipulate See {@link updateListField}. */ const updateList = (manipulate) => { Object.keys($entryDraft.currentValues).forEach((_locale) => { @@ -59,8 +58,8 @@ * @param {string} value New value. */ const addValue = (value) => { - updateList((list) => { - list.push(value); + updateList(({ valueList }) => { + valueList.push(value); }); }; @@ -69,8 +68,8 @@ * @param {number} index Target index. */ const removeValue = (index) => { - updateList((list) => { - list.splice(index, 1); + updateList(({ valueList }) => { + valueList.splice(index, 1); }); }; diff --git a/src/lib/services/contents/editor.js b/src/lib/services/contents/editor.js index d1aa72ba..9c3cc5a5 100644 --- a/src/lib/services/contents/editor.js +++ b/src/lib/services/contents/editor.js @@ -265,30 +265,49 @@ export const createDraft = (entry, defaultValues) => { ]), ), validities: Object.fromEntries(allLocales.map((locale) => [locale, {}])), + viewStates: Object.fromEntries(allLocales.map((locale) => [locale, {}])), }); }; /** - * Update the value in a list field. - * @param {LocaleCode} locale Target locale. - * @param {string} keyPath Dot-connected field name. - * @param {(list: any[]) => void} manipulate A function to manipulate the list, which takes one - * argument of the list itself. The typical usage is `list.splice()`. + * Unproxify & unflatten the given object. + * @param {Proxy | object} obj Original proxy or object. + * @returns {object} Processed object. */ -export const updateListField = (locale, keyPath, manipulate) => { - const unflattenObj = unflatten(get(entryDraft).currentValues[locale]); +const unflattenObj = (obj) => unflatten(JSON.parse(JSON.stringify(obj))); - // Traverse the object by decoding dot-connected `keyPath` - const list = keyPath.split('.').reduce((obj, key) => { +/** + * Traverse the given object by decoding dot-connected `keyPath`. + * @param {object} obj Unflatten object. + * @param {string} keyPath Dot-connected field name. + * @returns {object[]} Values. + */ +const getItemList = (obj, keyPath) => + keyPath.split('.').reduce((_obj, key) => { const _key = key.match(/^\d+$/) ? Number(key) : key; // Create a new array when adding a new item - obj[_key] ||= []; + _obj[_key] ||= []; + + return _obj[_key]; + }, obj); - return obj[_key]; - }, unflattenObj); +/** + * Update the value in a list field. + * @param {LocaleCode} locale Target locale. + * @param {string} keyPath Dot-connected field name. + * @param {({ valueList, viewList }) => void} manipulate A function to manipulate the list, which + * takes one object argument containing the value list and view state list. The typical usage is + * `list.splice()`. + */ +export const updateListField = (locale, keyPath, manipulate) => { + const currentValues = unflattenObj(get(entryDraft).currentValues[locale]); + const viewStates = unflattenObj(get(entryDraft).viewStates[locale]); - manipulate(list); + manipulate({ + valueList: getItemList(currentValues, keyPath), + viewList: getItemList(viewStates, keyPath), + }); entryDraft.update((draft) => ({ ...draft, @@ -299,9 +318,13 @@ export const updateListField = (locale, keyPath, manipulate) => { draft, prop: 'currentValues', locale, - target: flatten(unflattenObj), + target: flatten(currentValues), }), }, + viewStates: { + ...draft.viewStates, + [locale]: flatten(viewStates), + }, })); }; diff --git a/src/lib/typedefs.js b/src/lib/typedefs.js index e70eef3d..65ef2890 100644 --- a/src/lib/typedefs.js +++ b/src/lib/typedefs.js @@ -452,6 +452,12 @@ * @see https://www.npmjs.com/package/flatten */ +/** + * Flattened {@link EntryContent} object, where key is a key path, but value will be a file to be + * uploaded. + * @typedef {{ [key: string]: File }} FlattenedEntryContentFileList + */ + /** * Flattened {@link EntryContent} object, where key is a key path, but value will be the value’s * validity, using the same properties as the native HTML5 constraint validation. @@ -460,9 +466,9 @@ */ /** - * Flattened {@link EntryContent} object, where key is a key path, but value will be a file to be - * uploaded. - * @typedef {{ [key: string]: File }} FlattenedEntryContentFileList + * Flattened {@link EntryContent} object, where key is a key path, but value will be the value’s + * UI state. + * @typedef {{ [key: string]: any }} FlattenedEntryContentViewState */ /** @@ -476,13 +482,15 @@ * @property {CollectionFile} [collectionFile] File details. (File collection only) * @property {Entry} [originalEntry] Original entry or `undefined` if it’s a new entry draft. * @property {{ [key: LocaleCode]: FlattenedEntryContent }} originalValues Key is a locale code, - * value is an flattened object containing all the original field values. + * value is a flattened object containing all the original field values. * @property {{ [key: LocaleCode]: FlattenedEntryContent }} currentValues Key is a locale code, - * value is an flattened object containing all the current field values while editing. + * value is a flattened object containing all the current field values while editing. * @property {{ [key: LocaleCode]: FlattenedEntryContentFileList }} files Files to be uploaded. * @property {{ [key: LocaleCode]: FlattenedEntryContentValidityState }} validities Key is a locale - * code, value is an flattened object containing validation results of all the current field values + * code, value is a flattened object containing validation results of all the current field values * while editing. + * @property {{ [key: LocaleCode]: FlattenedEntryContentViewState }} viewStates Key is a locale + * code, value is a flattened object containing the UI state. */ /**