Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trix compatibility & allow developpers to setup more replacements on create #66

Merged
merged 8 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ DEPENDENCIES
shakapacker (= 7.2.2)
simple_form (~> 5.1)
simplecov
sqlite3
sqlite3 (~> 1.4)

BUNDLED WITH
2.5.3
2 changes: 1 addition & 1 deletion dev/gemfiles/rails-6.1.x.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion dev/gemfiles/rails-7.0.x.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
18 changes: 18 additions & 0 deletions npm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,24 @@ Event handlers receive a `CustomEvent` with following detail:

You can cancel an action within the `cocooned:before-<action>` 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({ 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

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.
Expand Down
31 changes: 31 additions & 0 deletions npm/__tests__/unit/cocooned/plugins/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,37 @@ describe('coreMixin', () => {
})
})

describe('replacements', () => {
it('returns default replacements', () => {
expect(given.extended.replacements).toEqual(expect.arrayContaining([
{ 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([
{ tag: 'trix-editor', attribute: 'input', delimiters: ['_'] }
]))
})

describe('when extended', () => {
beforeEach(() => {
given.extended.registerReplacement({ attribute: given.attribute, delimiters: [given.delimiter] })
})

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], tag: '*' }
]))
})
})
})

describe('when instanciated', () => {
beforeEach(() => {
document.body.innerHTML = given.html
Expand Down
11 changes: 8 additions & 3 deletions npm/__tests__/unit/cocooned/plugins/core/triggers/add.js
Original file line number Diff line number Diff line change
@@ -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 { Builder } from '@notus.sh/cocooned/src/cocooned/plugins/core/triggers/add/builder'
import { Add } from '@notus.sh/cocooned/src/cocooned/plugins/core/triggers/add'
import { jest } from '@jest/globals'
Expand All @@ -13,12 +14,16 @@ 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.extended.replacementsFor('new_item')
)
})
given('options', () => ({ builder: given.builder, node: given.addTrigger.parentElement, method: 'before' }))

Expand Down
22 changes: 20 additions & 2 deletions npm/__tests__/unit/cocooned/plugins/core/triggers/add/builder.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
/* 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 { faker } from '@cocooned/tests/support/faker'

describe('Builder', () => {
given('builder', () => new Builder(given.template.content, given.association))
given('association', () => 'new_person')
given('extended', () => coreMixin(Base))
given('builder', () => {
return new Builder(
given.template.content,
given.extended.replacementsFor('new_person')
)
})
given('id', () => faker.string.numeric(5))

const replacements = [
Expand Down Expand Up @@ -38,6 +45,17 @@ describe('Builder', () => {
<input type="text" id="contacts_${id}_new_attribute" name="contacts[${id}][new_attribute]">
</template>
`
},
{
desc: 'Trix editor',
template: `
<input type="hidden" id="contacts_new_person_name_trix_input">
<trix-editor id="contacts_new_person_name" input="contacts_new_person_name_trix_input"></trix-editor>
`,
expected: (id) => `
<input type="hidden" id="contacts_${id}_name_trix_input">
<trix-editor id="contacts_${id}_name" input="contacts_${id}_name_trix_input"></trix-editor>
`
}
]

Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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', () => `
Expand Down
28 changes: 28 additions & 0 deletions npm/src/cocooned/plugins/core.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
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 {
static registerReplacement ({ tag = '*', attribute, delimiters }) {
this.__replacements.push({ tag, attribute, delimiters })
}

static get replacements () {
return this.__replacements;
}

static replacementsFor (association) {
return this.replacements.map(r => new Replacement({ association, ...r }))
}

static get selectors () {
return {
...super.selectors,
Expand Down Expand Up @@ -31,6 +44,21 @@ const coreMixin = (Base) => class extends Base {
})
)
}

replacementsFor (association) {
return this.constructor.replacementsFor(association);
}

/* Protected and private attributes and methods */
static __replacements = [
// Default attributes
{ tag: 'label', attribute: 'for', delimiters: ['_'] },
{ tag: '*', attribute: 'id', delimiters: ['_'] },
{ tag: '*', attribute: 'name', delimiters: ['[', ']'] },

// Compatibility with Trix. See #65 on Github.
{ tag: 'trix-editor', attribute: 'input', delimiters: ['_'] },
];
}

export {
Expand Down
62 changes: 5 additions & 57 deletions npm/src/cocooned/plugins/core/triggers/add/builder.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -64,13 +11,14 @@ class Builder {
}

/* Protected and private attributes and methods */
#association
#documentFragment
#replacements

#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 => {
Expand Down
5 changes: 4 additions & 1 deletion npm/src/cocooned/plugins/core/triggers/add/extractor.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ class Extractor {
return null
}

return new Builder(template.content, `new_${this.#dataset.association}`)
return new Builder(
template.content,
this.#cocooned.replacementsFor(`new_${this.#dataset.association}`)
);
}

_extractCount () {
Expand Down
53 changes: 53 additions & 0 deletions npm/src/cocooned/plugins/core/triggers/add/replacement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Borrowed from Lodash
* See https://lodash.com/docs/#escapeRegExp
*/
const reRegExpChar = /[\\^$.*+?()[\]{}|]/g
const reHasRegExpChar = RegExp(reRegExpChar.source)

class Replacement {
attribute
tag

constructor ({ tag = '*', attribute, association, delimiters }) {
this.attribute = attribute
this.tag = tag

this.#association = association
this.#startDelimiter = delimiters[0]
this.#endDelimiter = delimiters[delimiters.length - 1]
}

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 */
#association
#startDelimiter
#endDelimiter

#replacement (id) {
return `${this.#startDelimiter}${id}${this.#endDelimiter}$1`
}

get #regexp () {
const escaped = this.#escape(`${this.#startDelimiter}${this.#association}${this.#endDelimiter}`)
return new RegExp(`${escaped}(.*?)`, 'g')
}

#escape (string) {
return (string && reHasRegExpChar.test(string))
? string.replace(reRegExpChar, '\\$&')
: (string || '')
}
}

export {
Replacement
}
Loading