From 57dc957b7ab241086ddcfa800724645175b521ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl-Ian?= Date: Fri, 17 May 2024 16:37:35 +0200 Subject: [PATCH 1/8] Add support for Trix editor attributes --- npm/__tests__/unit/cocooned/plugins/core.js | 33 +++++++++++ .../cocooned/plugins/core/triggers/add.js | 14 ++++- .../plugins/core/triggers/add/builder.js | 22 ++++++- .../plugins/core/triggers/add/extractor.js | 6 +- npm/src/cocooned/plugins/core.js | 23 ++++++++ .../plugins/core/triggers/add/builder.js | 58 +------------------ .../plugins/core/triggers/add/extractor.js | 8 ++- .../plugins/core/triggers/add/replacement.js | 51 ++++++++++++++++ 8 files changed, 152 insertions(+), 63 deletions(-) create mode 100644 npm/src/cocooned/plugins/core/triggers/add/replacement.js diff --git a/npm/__tests__/unit/cocooned/plugins/core.js b/npm/__tests__/unit/cocooned/plugins/core.js index fd09dfd..28a0b62 100644 --- a/npm/__tests__/unit/cocooned/plugins/core.js +++ b/npm/__tests__/unit/cocooned/plugins/core.js @@ -33,6 +33,39 @@ describe('coreMixin', () => { given('container', () => document.querySelector('[data-cocooned-container]')) given('template', () => '
') + describe('replacements', () => { + given('html', () => `
${given.template}
`) + + it('returns default replacements', () => { + expect(given.instance.replacements).toEqual(expect.arrayContaining([ + { attribute: 'for', delimiters: ['_'] }, + { attribute: 'id', delimiters: ['_'] }, + { attribute: 'name', delimiters: ['[', ']'] } + ])) + }) + + it('returns replacements for Trix compatibility', () => { + expect(given.instance.replacements).toEqual(expect.arrayContaining([ + { attribute: 'input', delimiters: ['_'] } + ])) + }) + + describe('when extended', () => { + beforeEach(() => { + given.extended.registerReplacement(given.attribute, given.delimiter) + }) + + given('attribute', () => faker.lorem.word()) + given('delimiter', () => faker.string.fromCharacters('_.-/')) + + it('returns registered additional replacement', () => { + expect(given.instance.replacements).toEqual(expect.arrayContaining([ + { attribute: given.attribute, delimiters: [given.delimiter] } + ])) + }) + }) + }) + describe('with add triggers', () => { describe('when inside container', () => { given('html', () => ` diff --git a/npm/__tests__/unit/cocooned/plugins/core/triggers/add.js b/npm/__tests__/unit/cocooned/plugins/core/triggers/add.js index ae85316..75078ae 100644 --- a/npm/__tests__/unit/cocooned/plugins/core/triggers/add.js +++ b/npm/__tests__/unit/cocooned/plugins/core/triggers/add.js @@ -1,7 +1,9 @@ /* global given */ -import { Base as Cocooned } from '@notus.sh/cocooned/src/cocooned/base' +import { coreMixin } from '@notus.sh/cocooned/src/cocooned/plugins/core' +import { Base } from '@notus.sh/cocooned/src/cocooned/base' import { Builder } from '@notus.sh/cocooned/src/cocooned/plugins/core/triggers/add/builder' +import { Replacement } from '@notus.sh/cocooned/src/cocooned/plugins/core/triggers/add/replacement' import { Add } from '@notus.sh/cocooned/src/cocooned/plugins/core/triggers/add' import { jest } from '@jest/globals' import { faker } from '@cocooned/tests/support/faker' @@ -13,14 +15,20 @@ import itBehavesLikeACancellableEvent from '@cocooned/tests/shared/events/cancel describe('Add', () => { beforeEach(() => { document.body.innerHTML = given.html }) - given('add', () => new Add(given.addTrigger, new Cocooned(given.container), given.options)) + given('extended', () => coreMixin(Base)) + given('add', () => new Add(given.addTrigger, new given.extended(given.container), given.options)) given('addTrigger', () => getAddLink(given.container)) given('container', () => document.querySelector('[data-cocooned-container]')) given('builder', () => { const template = document.querySelector('template[data-name="template"]') - return new Builder(template.content, 'new_item') + return new Builder(template.content, given.replacements) }) given('options', () => ({ builder: given.builder, node: given.addTrigger.parentElement, method: 'before' })) + given('replacements', () => { + return given.extended.replacements.map(r => { + return new Replacement(r.attribute, 'new_item', ...r.delimiters) + }) + }) given('template', () => '
') given('html', () => ` diff --git a/npm/__tests__/unit/cocooned/plugins/core/triggers/add/builder.js b/npm/__tests__/unit/cocooned/plugins/core/triggers/add/builder.js index 12a2a40..a5527aa 100644 --- a/npm/__tests__/unit/cocooned/plugins/core/triggers/add/builder.js +++ b/npm/__tests__/unit/cocooned/plugins/core/triggers/add/builder.js @@ -1,12 +1,21 @@ /* global given */ +import { coreMixin } from '@notus.sh/cocooned/src/cocooned/plugins/core' +import { Base } from '@notus.sh/cocooned/src/cocooned/base' import { Builder } from '@notus.sh/cocooned/src/cocooned/plugins/core/triggers/add/builder' +import { Replacement } from '@notus.sh/cocooned/src/cocooned/plugins/core/triggers/add/replacement' import { faker } from '@cocooned/tests/support/faker' describe('Builder', () => { - given('builder', () => new Builder(given.template.content, given.association)) + given('extended', () => coreMixin(Base)) + given('builder', () => new Builder(given.template.content, given.replacements)) given('association', () => 'new_person') given('id', () => faker.string.numeric(5)) + given('replacements', () => { + return given.extended.replacements.map(r => { + return new Replacement(r.attribute, 'new_person', ...r.delimiters) + }) + }) const replacements = [ { @@ -38,6 +47,17 @@ describe('Builder', () => { ` + }, + { + desc: 'Trix editor', + template: ` + + + `, + expected: (id) => ` + + + ` } ] diff --git a/npm/__tests__/unit/cocooned/plugins/core/triggers/add/extractor.js b/npm/__tests__/unit/cocooned/plugins/core/triggers/add/extractor.js index 43bbad2..9b2f158 100644 --- a/npm/__tests__/unit/cocooned/plugins/core/triggers/add/extractor.js +++ b/npm/__tests__/unit/cocooned/plugins/core/triggers/add/extractor.js @@ -1,6 +1,7 @@ /* global given */ -import { Base as Cocooned } from '@notus.sh/cocooned/src/cocooned/base' +import { coreMixin } from '@notus.sh/cocooned/src/cocooned/plugins/core' +import { Base } from '@notus.sh/cocooned/src/cocooned/base' import { Extractor } from '@notus.sh/cocooned/src/cocooned/plugins/core/triggers/add/extractor' import { Builder } from '@notus.sh/cocooned/src/cocooned/plugins/core/triggers/add/builder' import { deprecator } from '@notus.sh/cocooned/src/cocooned/deprecation' @@ -11,7 +12,8 @@ import { getAddLink } from '@cocooned/tests/support/helpers' describe('Extractor', () => { beforeEach(() => { document.body.innerHTML = given.html }) - given('extractor', () => new Extractor(given.addTrigger, new Cocooned(given.container))) + given('extended', () => coreMixin(Base)) + given('extractor', () => new Extractor(given.addTrigger, new given.extended(given.container))) given('container', () => document.querySelector('[data-cocooned-container]')) given('addTrigger', () => getAddLink(document)) given('html', () => ` diff --git a/npm/src/cocooned/plugins/core.js b/npm/src/cocooned/plugins/core.js index 61439ba..71a436e 100644 --- a/npm/src/cocooned/plugins/core.js +++ b/npm/src/cocooned/plugins/core.js @@ -3,6 +3,14 @@ import { Remove } from './core/triggers/remove.js' import { clickHandler, itemDelegatedClickHandler } from '../events/handlers.js' const coreMixin = (Base) => class extends Base { + static registerReplacement (attribute, ...delimiters) { + this.__replacements.push({ attribute, delimiters }) + } + + static get replacements () { + return this.__replacements; + } + static get selectors () { return { ...super.selectors, @@ -31,6 +39,21 @@ const coreMixin = (Base) => class extends Base { }) ) } + + get replacements () { + return this.constructor.replacements; + } + + /* Protected and private attributes and methods */ + static __replacements = [ + // Default attributes + { attribute: 'for', delimiters: ['_'] }, + { attribute: 'id', delimiters: ['_'] }, + { attribute: 'name', delimiters: ['[', ']'] }, + + // Compatibility with Trix. See #65 on Github. + { attribute: 'input', delimiters: ['_'] }, + ]; } export { diff --git a/npm/src/cocooned/plugins/core/triggers/add/builder.js b/npm/src/cocooned/plugins/core/triggers/add/builder.js index e850c14..87a2457 100644 --- a/npm/src/cocooned/plugins/core/triggers/add/builder.js +++ b/npm/src/cocooned/plugins/core/triggers/add/builder.js @@ -1,60 +1,7 @@ -/** - * Borrowed from Lodash - * See https://lodash.com/docs/#escapeRegExp - */ -const reRegExpChar = /[\\^$.*+?()[\]{}|]/g -const reHasRegExpChar = RegExp(reRegExpChar.source) - -class Replacement { - attribute - - constructor (attribute, name, startDelimiter, endDelimiter = null) { - this.attribute = attribute - - this.#name = name - this.#startDelimiter = startDelimiter - this.#endDelimiter = endDelimiter || startDelimiter - } - - apply (node, id) { - const value = node.getAttribute(this.attribute) - if (!this.#regexp.test(value)) { - return - } - - node.setAttribute(this.attribute, value.replace(this.#regexp, this.#replacement(id))) - } - - /* Protected and private attributes and methods */ - #name - #startDelimiter - #endDelimiter - - #replacement (id) { - return `${this.#startDelimiter}${id}${this.#endDelimiter}$1` - } - - get #regexp () { - const escaped = this.#escape(`${this.#startDelimiter}${this.#name}${this.#endDelimiter}`) - return new RegExp(`${escaped}(.*?)`, 'g') - } - - #escape (string) { - return (string && reHasRegExpChar.test(string)) - ? string.replace(reRegExpChar, '\\$&') - : (string || '') - } -} - class Builder { - constructor (documentFragment, association) { + constructor (documentFragment, replacements) { this.#documentFragment = documentFragment - this.#association = association - this.#replacements = [ - new Replacement('for', association, '_'), - new Replacement('id', association, '_'), - new Replacement('name', association, '[', ']') - ] + this.#replacements = replacements } build (id) { @@ -64,7 +11,6 @@ class Builder { } /* Protected and private attributes and methods */ - #association #documentFragment #replacements diff --git a/npm/src/cocooned/plugins/core/triggers/add/extractor.js b/npm/src/cocooned/plugins/core/triggers/add/extractor.js index 0735d69..d74bf57 100644 --- a/npm/src/cocooned/plugins/core/triggers/add/extractor.js +++ b/npm/src/cocooned/plugins/core/triggers/add/extractor.js @@ -1,4 +1,5 @@ import { Builder } from './builder.js' +import { Replacement } from './replacement.js' import { deprecator, Traverser } from '../../../../deprecation.js' class Extractor { @@ -39,7 +40,12 @@ class Extractor { return null } - return new Builder(template.content, `new_${this.#dataset.association}`) + const association = `new_${this.#dataset.association}`; + + return new Builder( + template.content, + this.#cocooned.replacements.map(r => new Replacement(r.attribute, association, ...r.delimiters)) + ); } _extractCount () { diff --git a/npm/src/cocooned/plugins/core/triggers/add/replacement.js b/npm/src/cocooned/plugins/core/triggers/add/replacement.js new file mode 100644 index 0000000..11a4ba6 --- /dev/null +++ b/npm/src/cocooned/plugins/core/triggers/add/replacement.js @@ -0,0 +1,51 @@ +/** + * Borrowed from Lodash + * See https://lodash.com/docs/#escapeRegExp + */ +const reRegExpChar = /[\\^$.*+?()[\]{}|]/g +const reHasRegExpChar = RegExp(reRegExpChar.source) + +class Replacement { + attribute + + constructor (attribute, name, startDelimiter, endDelimiter = null) { + this.attribute = attribute + + this.#name = name + this.#startDelimiter = startDelimiter + this.#endDelimiter = endDelimiter || startDelimiter + } + + apply (node, id) { + const value = node.getAttribute(this.attribute) + if (!this.#regexp.test(value)) { + return + } + + node.setAttribute(this.attribute, value.replace(this.#regexp, this.#replacement(id))) + } + + /* Protected and private attributes and methods */ + #name + #startDelimiter + #endDelimiter + + #replacement (id) { + return `${this.#startDelimiter}${id}${this.#endDelimiter}$1` + } + + get #regexp () { + const escaped = this.#escape(`${this.#startDelimiter}${this.#name}${this.#endDelimiter}`) + return new RegExp(`${escaped}(.*?)`, 'g') + } + + #escape (string) { + return (string && reHasRegExpChar.test(string)) + ? string.replace(reRegExpChar, '\\$&') + : (string || '') + } +} + +export { + Replacement +} From b16a6e493825eaa009a01e5119745f8bf205ba99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl-Ian?= Date: Fri, 17 May 2024 16:52:24 +0200 Subject: [PATCH 2/8] Simplify implementation --- npm/__tests__/unit/cocooned/plugins/core.js | 64 +++++++++---------- .../cocooned/plugins/core/triggers/add.js | 11 ++-- .../plugins/core/triggers/add/builder.js | 8 +-- npm/src/cocooned/plugins/core.js | 11 +++- .../plugins/core/triggers/add/extractor.js | 5 +- 5 files changed, 46 insertions(+), 53 deletions(-) diff --git a/npm/__tests__/unit/cocooned/plugins/core.js b/npm/__tests__/unit/cocooned/plugins/core.js index 28a0b62..8c45d39 100644 --- a/npm/__tests__/unit/cocooned/plugins/core.js +++ b/npm/__tests__/unit/cocooned/plugins/core.js @@ -23,49 +23,47 @@ describe('coreMixin', () => { }) }) - describe('when instanciated', () => { - beforeEach(() => { - document.body.innerHTML = given.html - given.instance.start() + describe('replacements', () => { + it('returns default replacements', () => { + expect(given.extended.replacements).toEqual(expect.arrayContaining([ + { attribute: 'for', delimiters: ['_'] }, + { attribute: 'id', delimiters: ['_'] }, + { attribute: 'name', delimiters: ['[', ']'] } + ])) }) - given('instance', () => new given.extended(given.container, given.options)) // eslint-disable-line new-cap - given('container', () => document.querySelector('[data-cocooned-container]')) - given('template', () => '
') - - describe('replacements', () => { - given('html', () => `
${given.template}
`) + it('returns replacements for Trix compatibility', () => { + expect(given.extended.replacements).toEqual(expect.arrayContaining([ + { attribute: 'input', delimiters: ['_'] } + ])) + }) - it('returns default replacements', () => { - expect(given.instance.replacements).toEqual(expect.arrayContaining([ - { attribute: 'for', delimiters: ['_'] }, - { attribute: 'id', delimiters: ['_'] }, - { attribute: 'name', delimiters: ['[', ']'] } - ])) + describe('when extended', () => { + beforeEach(() => { + given.extended.registerReplacement(given.attribute, given.delimiter) }) - it('returns replacements for Trix compatibility', () => { - expect(given.instance.replacements).toEqual(expect.arrayContaining([ - { attribute: 'input', delimiters: ['_'] } + given('attribute', () => faker.lorem.word()) + given('delimiter', () => faker.string.fromCharacters('_.-/')) + + it('returns registered additional replacement', () => { + expect(given.extended.replacements).toEqual(expect.arrayContaining([ + { attribute: given.attribute, delimiters: [given.delimiter] } ])) }) + }) + }) - describe('when extended', () => { - beforeEach(() => { - given.extended.registerReplacement(given.attribute, given.delimiter) - }) - - given('attribute', () => faker.lorem.word()) - given('delimiter', () => faker.string.fromCharacters('_.-/')) - - it('returns registered additional replacement', () => { - expect(given.instance.replacements).toEqual(expect.arrayContaining([ - { attribute: given.attribute, delimiters: [given.delimiter] } - ])) - }) - }) + describe('when instanciated', () => { + beforeEach(() => { + document.body.innerHTML = given.html + given.instance.start() }) + given('instance', () => new given.extended(given.container, given.options)) // eslint-disable-line new-cap + given('container', () => document.querySelector('[data-cocooned-container]')) + given('template', () => '
') + describe('with add triggers', () => { describe('when inside container', () => { given('html', () => ` diff --git a/npm/__tests__/unit/cocooned/plugins/core/triggers/add.js b/npm/__tests__/unit/cocooned/plugins/core/triggers/add.js index 75078ae..ed0f915 100644 --- a/npm/__tests__/unit/cocooned/plugins/core/triggers/add.js +++ b/npm/__tests__/unit/cocooned/plugins/core/triggers/add.js @@ -3,7 +3,6 @@ import { coreMixin } from '@notus.sh/cocooned/src/cocooned/plugins/core' import { Base } from '@notus.sh/cocooned/src/cocooned/base' import { Builder } from '@notus.sh/cocooned/src/cocooned/plugins/core/triggers/add/builder' -import { Replacement } from '@notus.sh/cocooned/src/cocooned/plugins/core/triggers/add/replacement' import { Add } from '@notus.sh/cocooned/src/cocooned/plugins/core/triggers/add' import { jest } from '@jest/globals' import { faker } from '@cocooned/tests/support/faker' @@ -21,14 +20,12 @@ describe('Add', () => { given('container', () => document.querySelector('[data-cocooned-container]')) given('builder', () => { const template = document.querySelector('template[data-name="template"]') - return new Builder(template.content, given.replacements) + return new Builder( + template.content, + given.extended.replacementsFor('new_item') + ) }) given('options', () => ({ builder: given.builder, node: given.addTrigger.parentElement, method: 'before' })) - given('replacements', () => { - return given.extended.replacements.map(r => { - return new Replacement(r.attribute, 'new_item', ...r.delimiters) - }) - }) given('template', () => '
') given('html', () => ` diff --git a/npm/__tests__/unit/cocooned/plugins/core/triggers/add/builder.js b/npm/__tests__/unit/cocooned/plugins/core/triggers/add/builder.js index a5527aa..830321e 100644 --- a/npm/__tests__/unit/cocooned/plugins/core/triggers/add/builder.js +++ b/npm/__tests__/unit/cocooned/plugins/core/triggers/add/builder.js @@ -3,19 +3,13 @@ import { coreMixin } from '@notus.sh/cocooned/src/cocooned/plugins/core' import { Base } from '@notus.sh/cocooned/src/cocooned/base' import { Builder } from '@notus.sh/cocooned/src/cocooned/plugins/core/triggers/add/builder' -import { Replacement } from '@notus.sh/cocooned/src/cocooned/plugins/core/triggers/add/replacement' import { faker } from '@cocooned/tests/support/faker' describe('Builder', () => { given('extended', () => coreMixin(Base)) given('builder', () => new Builder(given.template.content, given.replacements)) - given('association', () => 'new_person') given('id', () => faker.string.numeric(5)) - given('replacements', () => { - return given.extended.replacements.map(r => { - return new Replacement(r.attribute, 'new_person', ...r.delimiters) - }) - }) + given('replacements', () => given.extended.replacementsFor('new_person')) const replacements = [ { diff --git a/npm/src/cocooned/plugins/core.js b/npm/src/cocooned/plugins/core.js index 71a436e..a9283de 100644 --- a/npm/src/cocooned/plugins/core.js +++ b/npm/src/cocooned/plugins/core.js @@ -1,5 +1,6 @@ import { Add } from './core/triggers/add.js' import { Remove } from './core/triggers/remove.js' +import { Replacement } from './core/triggers/add/replacement.js' import { clickHandler, itemDelegatedClickHandler } from '../events/handlers.js' const coreMixin = (Base) => class extends Base { @@ -11,6 +12,12 @@ const coreMixin = (Base) => class extends Base { return this.__replacements; } + static replacementsFor (association) { + return this.replacements.map(r => { + return new Replacement(r.attribute, association, ...r.delimiters) + }) + } + static get selectors () { return { ...super.selectors, @@ -40,8 +47,8 @@ const coreMixin = (Base) => class extends Base { ) } - get replacements () { - return this.constructor.replacements; + replacementsFor (association) { + return this.constructor.replacementsFor(association); } /* Protected and private attributes and methods */ diff --git a/npm/src/cocooned/plugins/core/triggers/add/extractor.js b/npm/src/cocooned/plugins/core/triggers/add/extractor.js index d74bf57..704f73c 100644 --- a/npm/src/cocooned/plugins/core/triggers/add/extractor.js +++ b/npm/src/cocooned/plugins/core/triggers/add/extractor.js @@ -1,5 +1,4 @@ import { Builder } from './builder.js' -import { Replacement } from './replacement.js' import { deprecator, Traverser } from '../../../../deprecation.js' class Extractor { @@ -40,11 +39,9 @@ class Extractor { return null } - const association = `new_${this.#dataset.association}`; - return new Builder( template.content, - this.#cocooned.replacements.map(r => new Replacement(r.attribute, association, ...r.delimiters)) + this.#cocooned.replacementsFor(`new_${this.#dataset.association}`) ); } From 809f5543052ba7ecc0de57a9e1ba7f9bbd6e2781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl-Ian?= Date: Fri, 17 May 2024 17:11:12 +0200 Subject: [PATCH 3/8] Add some documentation --- npm/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/npm/README.md b/npm/README.md index 3e1df82..ed67e8c 100644 --- a/npm/README.md +++ b/npm/README.md @@ -204,6 +204,20 @@ Event handlers receive a `CustomEvent` with following detail: You can cancel an action within the `cocooned:before-` callback using `event.preventDefault()`. +## Attribute substitutions on create + +New items need to have a bunch of attributes updated before insert to make their `name`, `id` and `for` attributes consistent and unique. If you need additional substitutions to be made, you can configure them with: + +```javascript +import Cocooned from '@notus.sh/cocooned' + +// These 4 replacements are already set by default. +Cocooned.registerReplacement('name', '[', ']') +Cocooned.registerReplacement('id', '_') // Same start and end? Don't repeat yourself +Cocooned.registerReplacement('for', '_') +Cocooned.registerReplacement('input', '_') // Trix compatibility. +``` + ## Migration from a previous version These migrations steps only highlight major changes. When upgrading from a previous version, always refer to [the CHANGELOG](https://github.com/notus-sh/cocooned/blob/main/CHANGELOG.md) for new features and breaking changes. From 03c5c07c6575d61fcec71e5e80582e058aa2725d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl-Ian?= Date: Fri, 17 May 2024 17:14:19 +0200 Subject: [PATCH 4/8] Force sqlite3 version --- Gemfile | 2 +- Gemfile.lock | 2 +- dev/gemfiles/rails-6.1.x.gemfile | 2 +- dev/gemfiles/rails-7.0.x.gemfile | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 651ee83..3bfa381 100644 --- a/Gemfile +++ b/Gemfile @@ -11,7 +11,7 @@ group :development, :test do gem 'puma' gem 'rails' gem 'shakapacker', '7.2.2' - gem 'sqlite3' + gem 'sqlite3', '~> 1.4' gem 'formtastic', '~> 5.0' gem 'nokogiri' diff --git a/Gemfile.lock b/Gemfile.lock index a7ba200..323fb0c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -300,7 +300,7 @@ DEPENDENCIES shakapacker (= 7.2.2) simple_form (~> 5.1) simplecov - sqlite3 + sqlite3 (~> 1.4) BUNDLED WITH 2.5.3 diff --git a/dev/gemfiles/rails-6.1.x.gemfile b/dev/gemfiles/rails-6.1.x.gemfile index 4f87c50..0e3c50f 100644 --- a/dev/gemfiles/rails-6.1.x.gemfile +++ b/dev/gemfiles/rails-6.1.x.gemfile @@ -9,7 +9,7 @@ group :development, :test do gem 'puma' gem 'rails', '~> 6.1.0' gem 'shakapacker', '~> 7.1.0' - gem 'sqlite3' + gem 'sqlite3', '~> 1.4' gem 'formtastic', '~> 5.0' gem 'nokogiri' diff --git a/dev/gemfiles/rails-7.0.x.gemfile b/dev/gemfiles/rails-7.0.x.gemfile index 159a6d2..8e565ee 100644 --- a/dev/gemfiles/rails-7.0.x.gemfile +++ b/dev/gemfiles/rails-7.0.x.gemfile @@ -9,7 +9,7 @@ group :development, :test do gem 'puma' gem 'rails', '~> 7.0.0' gem 'shakapacker', '~> 7.1.0' - gem 'sqlite3' + gem 'sqlite3', '~> 1.4' gem 'formtastic', '~> 5.0' gem 'nokogiri' From 08153ee9ed0b91f9e85e1696a56bc7e5c52aebad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl-Ian?= Date: Sun, 19 May 2024 12:39:22 +0200 Subject: [PATCH 5/8] Rename Replacement constructor arguments and properties for clarity --- npm/src/cocooned/plugins/core/triggers/add/replacement.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/npm/src/cocooned/plugins/core/triggers/add/replacement.js b/npm/src/cocooned/plugins/core/triggers/add/replacement.js index 11a4ba6..f22a68f 100644 --- a/npm/src/cocooned/plugins/core/triggers/add/replacement.js +++ b/npm/src/cocooned/plugins/core/triggers/add/replacement.js @@ -8,10 +8,10 @@ const reHasRegExpChar = RegExp(reRegExpChar.source) class Replacement { attribute - constructor (attribute, name, startDelimiter, endDelimiter = null) { + constructor (attribute, association, startDelimiter, endDelimiter = null) { this.attribute = attribute - this.#name = name + this.#association = association this.#startDelimiter = startDelimiter this.#endDelimiter = endDelimiter || startDelimiter } @@ -26,7 +26,7 @@ class Replacement { } /* Protected and private attributes and methods */ - #name + #association #startDelimiter #endDelimiter @@ -35,7 +35,7 @@ class Replacement { } get #regexp () { - const escaped = this.#escape(`${this.#startDelimiter}${this.#name}${this.#endDelimiter}`) + const escaped = this.#escape(`${this.#startDelimiter}${this.#association}${this.#endDelimiter}`) return new RegExp(`${escaped}(.*?)`, 'g') } From 59648ac6c9d57b238bafa607c90357b9cfe49c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl-Ian?= Date: Sun, 19 May 2024 13:07:13 +0200 Subject: [PATCH 6/8] Introduce a tag argument to restrict matches --- npm/__tests__/unit/cocooned/plugins/core.js | 12 ++++++------ npm/src/cocooned/plugins/core.js | 16 +++++++--------- .../plugins/core/triggers/add/builder.js | 4 +++- .../plugins/core/triggers/add/replacement.js | 8 +++++--- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/npm/__tests__/unit/cocooned/plugins/core.js b/npm/__tests__/unit/cocooned/plugins/core.js index 8c45d39..9dbc50d 100644 --- a/npm/__tests__/unit/cocooned/plugins/core.js +++ b/npm/__tests__/unit/cocooned/plugins/core.js @@ -26,21 +26,21 @@ describe('coreMixin', () => { describe('replacements', () => { it('returns default replacements', () => { expect(given.extended.replacements).toEqual(expect.arrayContaining([ - { attribute: 'for', delimiters: ['_'] }, - { attribute: 'id', delimiters: ['_'] }, - { attribute: 'name', delimiters: ['[', ']'] } + { tag: 'label', attribute: 'for', delimiters: ['_'] }, + { tag: '*', attribute: 'id', delimiters: ['_'] }, + { tag: '*', attribute: 'name', delimiters: ['[', ']'] } ])) }) it('returns replacements for Trix compatibility', () => { expect(given.extended.replacements).toEqual(expect.arrayContaining([ - { attribute: 'input', delimiters: ['_'] } + { tag: 'trix-editor', attribute: 'input', delimiters: ['_'] } ])) }) describe('when extended', () => { beforeEach(() => { - given.extended.registerReplacement(given.attribute, given.delimiter) + given.extended.registerReplacement({ attribute: given.attribute, delimiters: [given.delimiter] }) }) given('attribute', () => faker.lorem.word()) @@ -48,7 +48,7 @@ describe('coreMixin', () => { it('returns registered additional replacement', () => { expect(given.extended.replacements).toEqual(expect.arrayContaining([ - { attribute: given.attribute, delimiters: [given.delimiter] } + { attribute: given.attribute, delimiters: [given.delimiter], tag: '*' } ])) }) }) diff --git a/npm/src/cocooned/plugins/core.js b/npm/src/cocooned/plugins/core.js index a9283de..145b2b4 100644 --- a/npm/src/cocooned/plugins/core.js +++ b/npm/src/cocooned/plugins/core.js @@ -4,8 +4,8 @@ import { Replacement } from './core/triggers/add/replacement.js' import { clickHandler, itemDelegatedClickHandler } from '../events/handlers.js' const coreMixin = (Base) => class extends Base { - static registerReplacement (attribute, ...delimiters) { - this.__replacements.push({ attribute, delimiters }) + static registerReplacement ({ tag = '*', attribute, delimiters }) { + this.__replacements.push({ tag, attribute, delimiters }) } static get replacements () { @@ -13,9 +13,7 @@ const coreMixin = (Base) => class extends Base { } static replacementsFor (association) { - return this.replacements.map(r => { - return new Replacement(r.attribute, association, ...r.delimiters) - }) + return this.replacements.map(r => new Replacement({ association, ...r })) } static get selectors () { @@ -54,12 +52,12 @@ const coreMixin = (Base) => class extends Base { /* Protected and private attributes and methods */ static __replacements = [ // Default attributes - { attribute: 'for', delimiters: ['_'] }, - { attribute: 'id', delimiters: ['_'] }, - { attribute: 'name', delimiters: ['[', ']'] }, + { tag: 'label', attribute: 'for', delimiters: ['_'] }, + { tag: '*', attribute: 'id', delimiters: ['_'] }, + { tag: '*', attribute: 'name', delimiters: ['[', ']'] }, // Compatibility with Trix. See #65 on Github. - { attribute: 'input', delimiters: ['_'] }, + { tag: 'trix-editor', attribute: 'input', delimiters: ['_'] }, ]; } diff --git a/npm/src/cocooned/plugins/core/triggers/add/builder.js b/npm/src/cocooned/plugins/core/triggers/add/builder.js index 87a2457..a7b58d0 100644 --- a/npm/src/cocooned/plugins/core/triggers/add/builder.js +++ b/npm/src/cocooned/plugins/core/triggers/add/builder.js @@ -16,7 +16,9 @@ class Builder { #applyReplacements (node, id) { this.#replacements.forEach(replacement => { - node.querySelectorAll(`*[${replacement.attribute}]`).forEach(node => replacement.apply(node, id)) + node.querySelectorAll(`${replacement.tag}[${replacement.attribute}]`).forEach(node => { + return replacement.apply(node, id) + }) }) node.querySelectorAll('template').forEach(template => { diff --git a/npm/src/cocooned/plugins/core/triggers/add/replacement.js b/npm/src/cocooned/plugins/core/triggers/add/replacement.js index f22a68f..0a57585 100644 --- a/npm/src/cocooned/plugins/core/triggers/add/replacement.js +++ b/npm/src/cocooned/plugins/core/triggers/add/replacement.js @@ -7,13 +7,15 @@ const reHasRegExpChar = RegExp(reRegExpChar.source) class Replacement { attribute + tag - constructor (attribute, association, startDelimiter, endDelimiter = null) { + constructor ({ tag = '*', attribute, association, delimiters }) { this.attribute = attribute + this.tag = tag this.#association = association - this.#startDelimiter = startDelimiter - this.#endDelimiter = endDelimiter || startDelimiter + this.#startDelimiter = delimiters[0] + this.#endDelimiter = delimiters[delimiters.length - 1] } apply (node, id) { From 01b96203da70f98be828451506576899a56ba22e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl-Ian?= Date: Sun, 19 May 2024 13:10:02 +0200 Subject: [PATCH 7/8] Update documentation --- npm/README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/npm/README.md b/npm/README.md index ed67e8c..980cd82 100644 --- a/npm/README.md +++ b/npm/README.md @@ -211,11 +211,15 @@ New items need to have a bunch of attributes updated before insert to make their ```javascript import Cocooned from '@notus.sh/cocooned' -// These 4 replacements are already set by default. -Cocooned.registerReplacement('name', '[', ']') -Cocooned.registerReplacement('id', '_') // Same start and end? Don't repeat yourself -Cocooned.registerReplacement('for', '_') -Cocooned.registerReplacement('input', '_') // Trix compatibility. +/** + * These 4 replacements are already set by default. + */ +Cocooned.registerReplacement({ attribute: 'name', delimiters: ['[', ']'] }) +// Same start and end? Don't repeat yourself. +Cocooned.registerReplacement({ attribute: 'id', delimiters: ['_'] }) +// You can target specific tags (else '*' is implied). +Cocooned.registerReplacement({ tag: 'label', attribute: 'for', delimiters: ['_'] }) +Cocooned.registerReplacement({ tag: 'trix-editor', attribute: 'input', delimiters: ['_'] }) ``` ## Migration from a previous version From 388ed27a0bd12c567dbd1c6a3804ed7dce6b5045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl-Ian?= Date: Mon, 20 May 2024 23:22:17 +0200 Subject: [PATCH 8/8] Minor fix in jest tests declaration --- .../unit/cocooned/plugins/core/triggers/add/builder.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/npm/__tests__/unit/cocooned/plugins/core/triggers/add/builder.js b/npm/__tests__/unit/cocooned/plugins/core/triggers/add/builder.js index 830321e..4ce9aad 100644 --- a/npm/__tests__/unit/cocooned/plugins/core/triggers/add/builder.js +++ b/npm/__tests__/unit/cocooned/plugins/core/triggers/add/builder.js @@ -7,9 +7,13 @@ import { faker } from '@cocooned/tests/support/faker' describe('Builder', () => { given('extended', () => coreMixin(Base)) - given('builder', () => new Builder(given.template.content, given.replacements)) + given('builder', () => { + return new Builder( + given.template.content, + given.extended.replacementsFor('new_person') + ) + }) given('id', () => faker.string.numeric(5)) - given('replacements', () => given.extended.replacementsFor('new_person')) const replacements = [ {