diff --git a/packages/core/src/Extension.ts b/packages/core/src/Extension.ts index 966121318ab..1bb06428a22 100644 --- a/packages/core/src/Extension.ts +++ b/packages/core/src/Extension.ts @@ -457,17 +457,17 @@ export class Extension { configure(options: Partial = {}) { // return a new instance so we can use the same extension // with different calls of `configure` - const extension = this.extend() - + const extension = this.extend({ + ...this.config, + addOptions() { + return mergeDeep(this.parent?.() || {}, options) as Options + }, + }) + + // Always preserve the current name + extension.name = this.name + // Set the parent to be our parent extension.parent = this.parent - extension.options = mergeDeep(this.options as Record, options) as Options - - extension.storage = callOrReturn( - getExtensionField(extension, 'addStorage', { - name: extension.name, - options: extension.options, - }), - ) return extension } @@ -483,7 +483,7 @@ export class Extension { extension.name = extendedConfig.name ? extendedConfig.name : extension.parent.name - if (extendedConfig.defaultOptions) { + if (extendedConfig.defaultOptions && Object.keys(extendedConfig.defaultOptions).length > 0) { console.warn( `[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${extension.name}".`, ) diff --git a/packages/core/src/Mark.ts b/packages/core/src/Mark.ts index 1b0c46d2bbf..9ff572b0faa 100644 --- a/packages/core/src/Mark.ts +++ b/packages/core/src/Mark.ts @@ -589,16 +589,17 @@ export class Mark { configure(options: Partial = {}) { // return a new instance so we can use the same extension // with different calls of `configure` - const extension = this.extend() - - extension.options = mergeDeep(this.options as Record, options) as Options + const extension = this.extend({ + ...this.config, + addOptions() { + return mergeDeep(this.parent?.() || {}, options) as Options + }, + }) - extension.storage = callOrReturn( - getExtensionField(extension, 'addStorage', { - name: extension.name, - options: extension.options, - }), - ) + // Always preserve the current name + extension.name = this.name + // Set the parent to be our parent + extension.parent = this.parent return extension } @@ -606,7 +607,7 @@ export class Mark { extend( extendedConfig: Partial> = {}, ) { - const extension = new Mark({ ...this.config, ...extendedConfig }) + const extension = new Mark(extendedConfig) extension.parent = this @@ -614,7 +615,7 @@ export class Mark { extension.name = extendedConfig.name ? extendedConfig.name : extension.parent.name - if (extendedConfig.defaultOptions) { + if (extendedConfig.defaultOptions && Object.keys(extendedConfig.defaultOptions).length > 0) { console.warn( `[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${extension.name}".`, ) diff --git a/packages/core/src/Node.ts b/packages/core/src/Node.ts index 7f3f02d35bc..c1fd865f71c 100644 --- a/packages/core/src/Node.ts +++ b/packages/core/src/Node.ts @@ -780,16 +780,17 @@ export class Node { configure(options: Partial = {}) { // return a new instance so we can use the same extension // with different calls of `configure` - const extension = this.extend() - - extension.options = mergeDeep(this.options as Record, options) as Options + const extension = this.extend({ + ...this.config, + addOptions() { + return mergeDeep(this.parent?.() || {}, options) as Options + }, + }) - extension.storage = callOrReturn( - getExtensionField(extension, 'addStorage', { - name: extension.name, - options: extension.options, - }), - ) + // Always preserve the current name + extension.name = this.name + // Set the parent to be our parent + extension.parent = this.parent return extension } @@ -797,7 +798,7 @@ export class Node { extend( extendedConfig: Partial> = {}, ) { - const extension = new Node({ ...this.config, ...extendedConfig }) + const extension = new Node(extendedConfig) extension.parent = this @@ -805,7 +806,7 @@ export class Node { extension.name = extendedConfig.name ? extendedConfig.name : extension.parent.name - if (extendedConfig.defaultOptions) { + if (extendedConfig.defaultOptions && Object.keys(extendedConfig.defaultOptions).length > 0) { console.warn( `[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${extension.name}".`, ) diff --git a/tests/cypress/integration/core/extendExtensions.spec.ts b/tests/cypress/integration/core/extendExtensions.spec.ts index 89103508adc..f57838a1929 100644 --- a/tests/cypress/integration/core/extendExtensions.spec.ts +++ b/tests/cypress/integration/core/extendExtensions.spec.ts @@ -1,354 +1,420 @@ /// -import { Extension, getExtensionField } from '@tiptap/core' +import { + Extension, getExtensionField, Mark, Node, +} from '@tiptap/core' describe('extend extensions', () => { - it('should define a config', () => { - const extension = Extension.create({ - addAttributes() { - return { + [Extension, Node, Mark].forEach(Extendable => { + describe(Extendable.create().type, () => { + it('should define a config', () => { + const extension = Extendable.create({ + addAttributes() { + return { + foo: {}, + } + }, + }) + + const attributes = getExtensionField(extension, 'addAttributes')() + + expect(attributes).to.deep.eq({ foo: {}, - } - }, - }) - - const attributes = getExtensionField(extension, 'addAttributes')() - - expect(attributes).to.deep.eq({ - foo: {}, - }) - }) - - it('should overwrite a config', () => { - const extension = Extension - .create({ - addAttributes() { - return { - foo: {}, - } - }, - }) - .extend({ - addAttributes() { - return { - bar: {}, - } - }, + }) }) - const attributes = getExtensionField(extension, 'addAttributes')() - - expect(attributes).to.deep.eq({ - bar: {}, - }) - }) - - it('should have a parent', () => { - const extension = Extension - .create({ - addAttributes() { - return { - foo: {}, - } - }, + it('should overwrite a config', () => { + const extension = Extendable + .create({ + addAttributes() { + return { + foo: {}, + } + }, + }) + .extend({ + addAttributes() { + return { + bar: {}, + } + }, + }) + + const attributes = getExtensionField(extension, 'addAttributes')() + + expect(attributes).to.deep.eq({ + bar: {}, + }) }) - const newExtension = extension - .extend({ - addAttributes() { - return { - bar: {}, - } - }, + it('should have a parent', () => { + const extension = Extendable + .create({ + addAttributes() { + return { + foo: {}, + } + }, + }) + + const newExtension = extension + .extend({ + addAttributes() { + return { + bar: {}, + } + }, + }) + + const parent = newExtension.parent + + expect(parent).to.eq(extension) }) - const parent = newExtension.parent - - expect(parent).to.eq(extension) - }) - - it('should merge configs', () => { - const extension = Extension - .create({ - addAttributes() { - return { - foo: {}, - } - }, - }) - .extend({ - addAttributes() { - return { - ...this.parent?.(), - bar: {}, - } - }, + it('should merge configs', () => { + const extension = Extendable + .create({ + addAttributes() { + return { + foo: {}, + } + }, + }) + .extend({ + addAttributes() { + return { + ...this.parent?.(), + bar: {}, + } + }, + }) + + const attributes = getExtensionField(extension, 'addAttributes')() + + expect(attributes).to.deep.eq({ + foo: {}, + bar: {}, + }) }) - const attributes = getExtensionField(extension, 'addAttributes')() - - expect(attributes).to.deep.eq({ - foo: {}, - bar: {}, - }) - }) - - it('should merge configs multiple times', () => { - const extension = Extension - .create({ - addAttributes() { - return { - foo: {}, - } - }, - }) - .extend({ - addAttributes() { - return { - ...this.parent?.(), - bar: {}, - } - }, + it('should merge configs multiple times', () => { + const extension = Extendable + .create({ + addAttributes() { + return { + foo: {}, + } + }, + }) + .extend({ + addAttributes() { + return { + ...this.parent?.(), + bar: {}, + } + }, + }) + .extend({ + addAttributes() { + return { + ...this.parent?.(), + baz: {}, + } + }, + }) + + const attributes = getExtensionField(extension, 'addAttributes')() + + expect(attributes).to.deep.eq({ + foo: {}, + bar: {}, + baz: {}, + }) }) - .extend({ - addAttributes() { - return { - ...this.parent?.(), - baz: {}, - } - }, + + it('should set parents multiple times', () => { + const grandparentExtension = Extendable + .create({ + addAttributes() { + return { + foo: {}, + } + }, + }) + + const parentExtension = grandparentExtension + .extend({ + addAttributes() { + return { + ...this.parent?.(), + bar: {}, + } + }, + }) + + const childExtension = parentExtension + .extend({ + addAttributes() { + return { + ...this.parent?.(), + baz: {}, + } + }, + }) + + expect(parentExtension.parent).to.eq(grandparentExtension) + expect(childExtension.parent).to.eq(parentExtension) }) - const attributes = getExtensionField(extension, 'addAttributes')() + it('should merge configs without direct parent configuration', () => { + const extension = Extendable + .create({ + addAttributes() { + return { + foo: {}, + } + }, + }) + .extend() + .extend({ + addAttributes() { + return { + ...this.parent?.(), + bar: {}, + } + }, + }) + + const attributes = getExtensionField(extension, 'addAttributes')() + + expect(attributes).to.deep.eq({ + foo: {}, + bar: {}, + }) + }) - expect(attributes).to.deep.eq({ - foo: {}, - bar: {}, - baz: {}, - }) - }) + it('should call ancestors only once', () => { + const callCounts = { + grandparent: 0, + parent: 0, + child: 0, + } - it('should set parents multiple times', () => { - const grandparentExtension = Extension - .create({ - addAttributes() { - return { - foo: {}, - } - }, + const extension = Extendable + .create({ + addAttributes() { + callCounts.grandparent += 1 + return { + foo: {}, + } + }, + }) + .extend({ + addAttributes() { + callCounts.parent += 1 + return { + ...this.parent?.(), + bar: {}, + } + }, + }) + .extend({ + addAttributes() { + callCounts.child += 1 + return { + ...this.parent?.(), + bar: {}, + } + }, + }) + + getExtensionField(extension, 'addAttributes')() + + expect(callCounts).to.deep.eq({ + grandparent: 1, + parent: 1, + child: 1, + }) }) - const parentExtension = grandparentExtension - .extend({ - addAttributes() { - return { - ...this.parent?.(), - bar: {}, - } - }, - }) + it('should call ancestors only once on configure', () => { + const callCounts = { + grandparent: 0, + parent: 0, + child: 0, + } - const childExtension = parentExtension - .extend({ - addAttributes() { - return { - ...this.parent?.(), + const extension = Extendable + .create({ + addAttributes() { + callCounts.grandparent += 1 + return { + foo: {}, + } + }, + }) + .extend({ + addAttributes() { + callCounts.parent += 1 + return { + ...this.parent?.(), + bar: {}, + } + }, + }) + .extend({ + addAttributes() { + callCounts.child += 1 + return { + ...this.parent?.(), + bar: {}, + } + }, + }) + .configure({ baz: {}, - } - }, - }) + }) - expect(parentExtension.parent).to.eq(grandparentExtension) - expect(childExtension.parent).to.eq(parentExtension) - }) + getExtensionField(extension, 'addAttributes')() - it('should merge configs without direct parent configuration', () => { - const extension = Extension - .create({ - addAttributes() { - return { - foo: {}, - } - }, + expect(callCounts).to.deep.eq({ + grandparent: 1, + parent: 1, + child: 1, + }) }) - .extend() - .extend({ - addAttributes() { - return { - ...this.parent?.(), - bar: {}, - } - }, - }) - - const attributes = getExtensionField(extension, 'addAttributes')() - expect(attributes).to.deep.eq({ - foo: {}, - bar: {}, - }) - }) + it('should use grandparent as parent on configure (not parent)', () => { + const grandparentExtension = Extendable + .create({ + addAttributes() { + return { + foo: {}, + } + }, + }) + + const parentExtension = grandparentExtension + .extend({ + addAttributes() { + return { + ...this.parent?.(), + bar: {}, + } + }, + }) + + const childExtension = parentExtension + .configure({ + baz: {}, + }) - it('should call ancestors only once', () => { - const callCounts = { - grandparent: 0, - parent: 0, - child: 0, - } - - const extension = Extension - .create({ - addAttributes() { - callCounts.grandparent += 1 - return { - foo: {}, - } - }, - }) - .extend({ - addAttributes() { - callCounts.parent += 1 - return { - ...this.parent?.(), - bar: {}, - } - }, + expect(parentExtension.parent).to.eq(grandparentExtension) + expect(childExtension.parent).to.eq(grandparentExtension) }) - .extend({ - addAttributes() { - callCounts.child += 1 - return { - ...this.parent?.(), - bar: {}, - } - }, - }) - - getExtensionField(extension, 'addAttributes')() - expect(callCounts).to.deep.eq({ - grandparent: 1, - parent: 1, - child: 1, - }) - }) + it('should use parent\'s config on `configure`', () => { + const grandparentExtension = Extendable + .create({ + name: 'grandparent', + addAttributes() { + return { + foo: {}, + } + }, + }) + + const parentExtension = grandparentExtension + .extend({ + name: 'parent', + addAttributes() { + return { + ...this.parent?.(), + bar: {}, + } + }, + }) + + const childExtension = parentExtension + .configure({ + baz: {}, + }) - it('should call ancestors only once on configure', () => { - const callCounts = { - grandparent: 0, - parent: 0, - child: 0, - } - - const extension = Extension - .create({ - addAttributes() { - callCounts.grandparent += 1 - return { - foo: {}, - } - }, - }) - .extend({ - addAttributes() { - callCounts.parent += 1 - return { - ...this.parent?.(), - bar: {}, - } - }, - }) - .extend({ - addAttributes() { - callCounts.child += 1 - return { - ...this.parent?.(), - bar: {}, - } - }, - }) - .configure({ - baz: {}, + expect(childExtension.config.name).to.eq('parent') }) - getExtensionField(extension, 'addAttributes')() + it('should allow extending a configure', () => { - expect(callCounts).to.deep.eq({ - grandparent: 1, - parent: 1, - child: 1, - }) - }) - - it('should use grandparent as parent on configure (not parent)', () => { - const grandparentExtension = Extension - .create({ - addAttributes() { - return { - foo: {}, - } - }, - }) + const parentExtension = Extendable + .create({ - const parentExtension = grandparentExtension - .extend({ - addAttributes() { - return { - ...this.parent?.(), - bar: {}, - } - }, - }) - - const childExtension = parentExtension - .configure({ - baz: {}, - }) + addAttributes() { + return { foo: 'bar' } + }, + }) - expect(parentExtension.parent).to.eq(grandparentExtension) - expect(childExtension.parent).to.eq(grandparentExtension) - }) + const childExtension = parentExtension + .configure().extend() - it('should use parent\'s config on `configure`', () => { - const grandparentExtension = Extension - .create({ - name: 'grandparent', - addAttributes() { - return { - foo: {}, - } - }, - }) + const attributes = getExtensionField(childExtension, 'addAttributes')() - const parentExtension = grandparentExtension - .extend({ - name: 'parent', - addAttributes() { - return { - ...this.parent?.(), - bar: {}, - } - }, + expect(attributes).to.deep.eq({ + foo: 'bar', + }) }) - const childExtension = parentExtension - .configure({ - baz: {}, + it('should allow calling this.parent when extending a configure', () => { + + const parentExtension = Extendable + .create({ + name: 'parentExtension', + addAttributes() { + return { + foo: {}, + } + }, + }) + + const childExtension = parentExtension + .configure({}).extend({ + addAttributes() { + return { + ...this.parent?.(), + bar: {}, + } + }, + }) + + const attributes = getExtensionField(childExtension, 'addAttributes')() + + expect(attributes).to.deep.eq({ + foo: {}, + bar: {}, + }) }) - expect(childExtension.config.name).to.eq('parent') - }) - - it('should inherit config on configure', () => { - - const parentExtension = Extension - .create({ - name: 'did-inherit', + it('should deeply merge options when extending a configured extension', () => { + const parentExtension = Extendable + .create({ + name: 'parentExtension', + addOptions() { + return { defaultOptions: 'is-overwritten' } + }, + }) + + const childExtension = parentExtension + .configure({ configuredOptions: 'exists-too' }).extend({ + name: 'childExtension', + addOptions() { + return { ...this.parent?.(), additionalOptions: 'exist-too' } + }, + }) + + expect(childExtension.options).to.deep.eq({ + configuredOptions: 'exists-too', + additionalOptions: 'exist-too', + }) }) - - const childExtension = parentExtension - .configure() - - expect(childExtension.config.name).to.eq('did-inherit') + }) }) })