'use strict'
interface ISfCollectionConfig {
collectionsSelector: string // Default: 'div[data-prototype]',
entry: {
add: {
enabled: boolean // Default: true | Possible override by DOM attribute: data-entry-add-enabled
prototype: string // Default: '' | Possible override by DOM attribute: data-entry-add-prototype
class: string // Default: 'btn btn-primary btn-sm mt-2' | Possible override by DOM attribute: data-entry-add-class
label: string // Default: 'Add' | Possible override by DOM attribute: data-entry-add-label
customFn:
| ((
collectionElt: Element,
entryAddElt: Element,
templateContentEntry: DocumentFragment,
cfg: ISfCollectionConfig
) => void)
| null
onBeforeFn:
| ((
collectionElt: Element,
entryAddElt: Element,
templateContentEntry: DocumentFragment
) => void)
| null
onAfterFn: ((collectionElt: Element, entryAddElt: Element) => void) | null
}
remove: {
enabled: boolean // Default: true | Possible override by DOM attribute: data-entry-remove-enabled
prototype: string // Default: '' | Possible override by DOM attribute: data-entry-remove-prototype
class: string // Default: 'btn btn-danger btn-sm mt-2' | Possible override by DOM attribute: data-entry-remove-class
label: string // Default: 'Remove' | Possible override by DOM attribute: data-entry-remove-label
customFn:
| ((
collectionElt: Element,
entryRemoveElt: Element,
cfg: ISfCollectionConfig
) => void)
| null
onAfterFn:
| ((collectionElt: Element, entryRemoveElt: Element) => void)
| null
}
}
}
const a2lix_lib: Record = {}
a2lix_lib.sfCollection = (() => {
enum ENTRY_ACTION {
ADD = 'add',
REMOVE = 'remove',
}
const CONFIG_DEFAULT: ISfCollectionConfig = {
collectionsSelector: 'div[data-prototype]',
entry: {
add: {
enabled: true,
prototype: ``,
class: 'btn btn-primary btn-sm mt-2',
label: 'Add',
customFn: null,
onBeforeFn: null,
onAfterFn: null,
},
remove: {
enabled: true,
prototype: ``,
class: 'btn btn-danger btn-sm mt-2',
label: 'Remove',
customFn: null,
onAfterFn: null,
},
},
}
const init = (config = CONFIG_DEFAULT) => {
if (!('content' in document.createElement('template'))) {
console.error('HTML template will not working...')
return
}
const cfg: ISfCollectionConfig = {
...CONFIG_DEFAULT,
...config,
entry: {
add: { ...CONFIG_DEFAULT.entry.add, ...(config.entry?.add || {}) },
remove: {
...CONFIG_DEFAULT.entry.remove,
...(config.entry?.remove || {}),
},
},
}
proceedCollectionElts(
document.querySelectorAll(cfg.collectionsSelector),
cfg
)
}
const proceedCollectionElts = (
collectionElts: NodeListOf,
cfg: ISfCollectionConfig
) => {
if (!collectionElts.length) {
return
}
collectionElts.forEach((collectionElt) => {
proceedCollectionElt(collectionElt, cfg)
})
}
const proceedCollectionElt = (
collectionElt: Element,
cfg: ISfCollectionConfig
) => {
collectionElt.setAttribute(
'data-entry-index',
collectionElt.children.length + ''
)
if (
collectionElt.getAttribute('data-entry-add-enabled') ??
cfg.entry.add.enabled
) {
appendEntryAddElt(collectionElt, cfg.entry.add)
}
if (
collectionElt.getAttribute('data-entry-remove-enabled') ??
cfg.entry.remove.enabled
) {
appendEntryRemoveElts(collectionElt, cfg.entry.remove)
}
collectionElt.addEventListener('click', (evt) =>
handleEntryActionClick(evt, cfg)
)
}
const appendEntryAddElt = (
collectionElt: Element,
entryAddCfg: ISfCollectionConfig['entry']['add']
) => {
const entryAddHtml = (
collectionElt.getAttribute('data-entry-add-prototype') ??
entryAddCfg.prototype
)
.replace(
/__class__/g,
collectionElt.getAttribute('data-entry-add-class') ?? entryAddCfg.class
)
.replace(
/__label__/g,
collectionElt.getAttribute('data-entry-add-label') ?? entryAddCfg.label
)
collectionElt.appendChild(createTemplateContent(entryAddHtml))
}
const appendEntryRemoveElts = (
collectionElt: Element,
entryRemoveCfg: ISfCollectionConfig['entry']['remove']
) => {
const templateContentEntryRemove = getTemplateContentEntryRemove(
collectionElt,
entryRemoveCfg
)
Array.from(collectionElt.children)
.filter((entryElt) => !entryElt.hasAttribute('data-entry-action'))
.forEach((entryElt) => {
entryElt.appendChild(templateContentEntryRemove.cloneNode(true))
})
}
const getTemplateContentEntryRemove = (
collectionElt: Element,
entryRemoveCfg: ISfCollectionConfig['entry']['remove']
) => {
const entryRemoveHtml = (
collectionElt.getAttribute('data-entry-remove-prototype') ??
entryRemoveCfg.prototype
)
.replace(
/__class__/g,
collectionElt.getAttribute('data-entry-remove-class') ??
entryRemoveCfg.class
)
.replace(
/__label__/g,
collectionElt.getAttribute('data-entry-remove-label') ??
entryRemoveCfg.label
)
return createTemplateContent(entryRemoveHtml)
}
const handleEntryActionClick = (evt: Event, cfg: ISfCollectionConfig) => {
if (!(evt.target as Element).hasAttribute('data-entry-action')) {
return
}
evt.preventDefault()
evt.stopPropagation()
const collectionElt = (evt.currentTarget as Element).closest(
cfg.collectionsSelector
)!
switch ((evt.target as Element).getAttribute('data-entry-action')) {
case ENTRY_ACTION.ADD:
handleEntryActionAddClick(collectionElt, evt.target as Element, cfg)
break
case ENTRY_ACTION.REMOVE:
handleEntryActionRemoveClick(collectionElt, evt.target as Element, cfg)
break
}
}
const handleEntryActionAddClick = (
collectionElt: Element,
entryAddElt: Element,
cfg: ISfCollectionConfig
) => {
const templateContentEntry = getTemplateContentEntry(collectionElt, cfg)
if (cfg.entry.add.customFn) {
cfg.entry.add.customFn(
collectionElt,
entryAddElt,
templateContentEntry,
cfg
)
return
}
addEntry(collectionElt, entryAddElt, templateContentEntry, cfg)
}
const handleEntryActionRemoveClick = (
collectionElt: Element,
entryRemoveElt: Element,
cfg: ISfCollectionConfig
) => {
if (cfg.entry.remove.customFn) {
cfg.entry.remove.customFn(collectionElt, entryRemoveElt, cfg)
return
}
removeEntry(collectionElt, entryRemoveElt, cfg)
}
const getTemplateContentEntry = (
collectionElt: Element,
cfg: ISfCollectionConfig
) => {
const entryIndex = collectionElt.getAttribute('data-entry-index') ?? 0
collectionElt.setAttribute('data-entry-index', +entryIndex + 1 + '')
const prototypeName =
collectionElt.getAttribute('data-prototype-name') ?? '__name__'
const entryHtml = collectionElt
.getAttribute('data-prototype')!
.replace(
new RegExp(`${prototypeName}label__`, 'g'),
`!New! ${entryIndex}`
)
.replace(new RegExp(prototypeName, 'g'), entryIndex + '')
const templateContentEntry = createTemplateContent(entryHtml)
if (
collectionElt.getAttribute('data-entry-remove-enabled') ??
cfg.entry.remove.enabled
) {
templateContentEntry.firstChild!.appendChild(
getTemplateContentEntryRemove(collectionElt, cfg.entry.remove)
)
}
return templateContentEntry
}
const addEntry = (
collectionElt: Element,
entryAddElt: Element,
templateContentEntry: DocumentFragment,
cfg: ISfCollectionConfig
) => {
cfg.entry.add.onBeforeFn?.(collectionElt, entryAddElt, templateContentEntry)
entryAddElt.parentElement!.insertBefore(templateContentEntry, entryAddElt)
proceedCollectionElts(
entryAddElt.previousElementSibling!.querySelectorAll(
cfg.collectionsSelector
),
cfg
)
cfg.entry.add.onAfterFn?.(collectionElt, entryAddElt)
}
const removeEntry = (
collectionElt: Element,
entryRemoveElt: Element,
cfg: ISfCollectionConfig
) => {
entryRemoveElt.parentElement!.remove()
cfg.entry.remove.onAfterFn?.(collectionElt, entryRemoveElt)
}
/**
* HELPERS
*/
const createTemplateContent = (html: string) => {
const template = document.createElement('template')
template.innerHTML = html.trim()
return template.content
}
return {
init,
}
})()
export default a2lix_lib