diff --git a/resources/js/components/fieldtypes/bard/BardFieldtype.vue b/resources/js/components/fieldtypes/bard/BardFieldtype.vue index 86b8004a09e..019173a80a5 100644 --- a/resources/js/components/fieldtypes/bard/BardFieldtype.vue +++ b/resources/js/components/fieldtypes/bard/BardFieldtype.vue @@ -452,11 +452,14 @@ export default { } }, - value(value) { + value(value, oldValue) { if (!this.editor) return; if (this.valueMatchesEditor(value)) return; + // A repeat of the old value would undo typing that's still debounced. + if (JSON.stringify(value) === JSON.stringify(oldValue)) return; + const oldContent = this.editor.getJSON(); // Sets sync their own values into the editor, so a rebuild for those would only destroy their fields. @@ -472,7 +475,7 @@ export default { } this.editor.commands.clearContent(false); - this.editor.commands.setContent(this.valueToContent(value), true); + this.editor.commands.setContent(this.valueToContent(clone(value)), true); }, readOnly(readOnly) { @@ -1004,6 +1007,13 @@ export default { this.editor.view.dispatch(tr); }, + // Set field edits are already in the container. Emitting them back would hand each field a copy of its own value. + update(value) { + if (this.editor && this.valueMatchesEditor(this.value) && this.valueMatchesEditor(value)) return; + + this.$emit('update:value', value); + }, + // Parsing normalizes attrs that the stored value omits but ProseMirror serializes. valueMatchesEditor(value) { const content = this.valueToContent(value); diff --git a/resources/js/tests/components/fieldtypes/bard/BardFieldtype.test.js b/resources/js/tests/components/fieldtypes/bard/BardFieldtype.test.js index f143c44b0b5..b40305e6807 100644 --- a/resources/js/tests/components/fieldtypes/bard/BardFieldtype.test.js +++ b/resources/js/tests/components/fieldtypes/bard/BardFieldtype.test.js @@ -213,6 +213,17 @@ test('the editor is left alone when the value only differs by omitted attribute expect(wrapper.vm.editor.state.doc).toBe(doc); }); +test('typing still waiting for the debounced update is kept when the same value arrives again', async () => { + const wrapper = await mountField(); + + await type(wrapper, 'Zero. '); + await wrapper.setProps({ value: clone(value) }); + await waitForDebounce(); + + expect(wrapper.vm.editor.getText()).toBe('Zero. One two three four five.'); + expect(wrapper.emitted('update:value')?.at(-1)).toEqual([wrapper.vm.editor.getJSON().content]); +}); + // With remove_empty_nodes, a save strips empty nodes. Deleting them in place keeps the set fields alive. test('empty nodes the value no longer has are deleted in place', async () => { const text = { type: 'paragraph', content: [{ type: 'text', text: 'Text' }] }; @@ -277,6 +288,52 @@ test('set field edits made while an update is debounced are kept', async () => { expect(wrapper.vm.editor.getJSON().content[1].attrs.values.label).toBe('ABC'); }); +test('set field edits are not emitted back into the container', async () => { + const { wrapper, values } = await mountFieldInContainer({ sets }, [ + { type: 'paragraph', content: [{ type: 'text', text: 'One' }] }, + // As saved, without attribute defaults. + { type: 'set', attrs: { id: 'set-a', values: { type: 'card', label: 'A' } } }, + ]); + + await editSetLabel(values, 'AB'); + await waitForDebounce(); + + expect(wrapper.vm.editor.getJSON().content[1].attrs.values.label).toBe('AB'); + expect(wrapper.emitted('update:value')).toBeUndefined(); +}); + +test('set attributes changed in the editor are still emitted', async () => { + const { wrapper, values } = await mountFieldInContainer({ sets }, [ + { type: 'paragraph', content: [{ type: 'text', text: 'One' }] }, + set('set-a', 'A'), + ]); + const editor = wrapper.vm.editor; + + editor.view.dispatch(editor.state.tr.setNodeAttribute(editor.state.doc.child(0).nodeSize, 'enabled', false)); + await waitForDebounce(); + + expect(values.value.content[1].attrs.enabled).toBe(false); +}); + +test('set field edits made after the editor was rebuilt are kept', async () => { + const { wrapper, values } = await mountFieldInContainer({ sets }, [ + { type: 'paragraph', content: [{ type: 'text', text: 'One' }] }, + set('set-a', 'A'), + ]); + + data_set(values.value, 'content', [{ type: 'paragraph', content: [{ type: 'text', text: 'Two' }] }, set('set-a', 'A')]); + await wrapper.setProps({ value: values.value.content }); + await waitForDebounce(); + + wrapper.vm.editor.commands.insertContentAt(1, 'Then '); + await nextTick(); + await editSetLabel(values, 'AB'); + await waitForDebounce(); + + expect(values.value.content[0].content[0].text).toBe('Then Two'); + expect(values.value.content[1].attrs.values.label).toBe('AB'); +}); + test('each set field edit can be undone', async () => { const { wrapper, values } = await mountFieldInContainer({ sets }, [ { type: 'paragraph', content: [{ type: 'text', text: 'One' }] },