| 1 |
/* Immutable editing of converted carousel collections. Existing templates need no migration. */ |
| 2 |
(function (scope) { |
| 3 |
'use strict'; |
| 4 |
function copy(value) { return JSON.parse(JSON.stringify(value)); } |
| 5 |
function classes(node) { return String((node.attrs || {}).class || '').split(/\s+/); } |
| 6 |
function references(node) { |
| 7 |
var found = []; |
| 8 |
function scan(value) { |
| 9 |
if (!value || typeof value !== 'object') return; |
| 10 |
if (typeof value.field === 'string' && found.indexOf(value.field) < 0) found.push(value.field); |
| 11 |
Object.keys(value).forEach(function (key) { if (key !== 'field') scan(value[key]); }); |
| 12 |
} |
| 13 |
scan(node); return found; |
| 14 |
} |
| 15 |
function label(field, context) { |
| 16 |
if (field.type === 'image') return /avatar|author|testimonial/.test(context) ? 'Portrait' : 'Image'; |
| 17 |
if (field.type === 'url') return 'Link URL'; |
| 18 |
var tokens = context.split(/\s+/).reverse(); |
| 19 |
for (var i = 0; i < tokens.length; i++) { |
| 20 |
var token = tokens[i]; |
| 21 |
if (/extra-label|timeline-label/.test(token)) return 'Date / label'; |
| 22 |
if (/subtitle|position|job/.test(token)) return 'Subtitle'; |
| 23 |
if (/testimonial.*(?:title|name)|author-name|person-name/.test(token)) return 'Name'; |
| 24 |
if (/testimonial.*description|quote/.test(token)) return 'Quote'; |
| 25 |
if (/description|excerpt/.test(token)) return 'Description'; |
| 26 |
if (/title|heading/.test(token)) return 'Title'; |
| 27 |
if (/caption/.test(token)) return 'Caption'; |
| 28 |
} |
| 29 |
return field.type === 'number' ? 'Value' : field.type === 'boolean' ? 'Enabled' : 'Text'; |
| 30 |
} |
| 31 |
function fieldsFor(node, fields) { |
| 32 |
var map = {}, contexts = {}; |
| 33 |
(fields || []).forEach(function (f) { map[f.id] = f; }); |
| 34 |
function walk(n, context) { |
| 35 |
context = context + ' ' + classes(n).join(' '); |
| 36 |
if (n.field) contexts[n.field] = context; |
| 37 |
references(n.attrs || {}).forEach(function (id) { if (!contexts[id]) contexts[id] = context; }); |
| 38 |
(n.children || []).forEach(function (c) { walk(c, context); }); |
| 39 |
} |
| 40 |
walk(node, ''); |
| 41 |
return references(node).map(function (id) { |
| 42 |
return map[id] ? Object.assign({}, map[id], { label: label(map[id], contexts[id] || '') }) : null; |
| 43 |
}).filter(Boolean); |
| 44 |
} |
| 45 |
function groups(attributes) { |
| 46 |
var result = []; |
| 47 |
function walk(nodes, path) { |
| 48 |
(nodes || []).forEach(function (node, i) { |
| 49 |
var here = path.concat(i); |
| 50 |
if (classes(node).indexOf('swiper-wrapper') >= 0) { |
| 51 |
var items = []; |
| 52 |
(node.children || []).forEach(function (child, at) { |
| 53 |
if (classes(child).indexOf('swiper-slide') < 0 || classes(child).indexOf('swiper-slide-duplicate') >= 0) return; |
| 54 |
var fields = fieldsFor(child, attributes.fields); |
| 55 |
var title = fields.find(function (f) { return /Title|Name|Date/.test(f.label) && f.value; }); |
| 56 |
items.push({at: at, node: child, fields: fields, |
| 57 |
key: (child.attrs || {})['data-bkbg-item-id'] || references(child).join('-') || String(at), |
| 58 |
title: title ? String(title.value).replace(/<[^>]*>/g, '').replace(/&[^;]+;/g, ' ').trim().slice(0,60) : ''}); |
| 59 |
}); |
| 60 |
result.push({path: here, items: items}); |
| 61 |
} else walk(node.children, here); |
| 62 |
}); |
| 63 |
} |
| 64 |
walk(attributes.tree, []); return result; |
| 65 |
} |
| 66 |
function atPath(tree, path) { |
| 67 |
var node, nodes = tree; |
| 68 |
path.forEach(function (index) { node = nodes[index]; nodes = node.children; }); |
| 69 |
return node; |
| 70 |
} |
| 71 |
function change(attributes, path, operation, index, token) { |
| 72 |
var tree = copy(attributes.tree || []), fields = copy(attributes.fields || []); |
| 73 |
var track = atPath(tree, path), items = (track.children || []).filter(function (n) { |
| 74 |
return classes(n).indexOf('swiper-slide') >= 0 && classes(n).indexOf('swiper-slide-duplicate') < 0; |
| 75 |
}); |
| 76 |
var templates = copy(!Array.isArray(attributes.collectionTemplates) && attributes.collectionTemplates || {}), templateKey = path.join('-'); |
| 77 |
var template = templates[templateKey]; |
| 78 |
var item = items[index] || (operation === 'add' && template ? template.node : null); |
| 79 |
if (!item) return null; |
| 80 |
var at = track.children.indexOf(item); |
| 81 |
if (operation === 'remove') { |
| 82 |
if (items.length === 1) templates[templateKey] = {node:copy(item),fields:fields.filter(function(f){return references(item).indexOf(f.id)>=0;})}; |
| 83 |
var removed = references(item); track.children.splice(at,1); |
| 84 |
var retained = references(tree); |
| 85 |
fields = fields.filter(function (f) { return removed.indexOf(f.id) < 0 || retained.indexOf(f.id) >= 0; }); |
| 86 |
} else if (operation === 'up' || operation === 'down') { |
| 87 |
var target = index + (operation === 'up' ? -1 : 1); |
| 88 |
if (!items[target]) return null; |
| 89 |
var otherAt = track.children.indexOf(items[target]); |
| 90 |
track.children[at] = items[target]; track.children[otherAt] = item; |
| 91 |
} else if (operation === 'add' || operation === 'duplicate') { |
| 92 |
token = token || Date.now().toString(36) + Math.random().toString(36).slice(2,8); |
| 93 |
var clone = copy(item), ids = {}, refs = references(item); |
| 94 |
var existing = new Set(fields.map(function (f) { return f.id; })); |
| 95 |
refs.forEach(function (id, i) { |
| 96 |
var original = fields.find(function (f) { return f.id === id; }) || (template && template.fields.find(function(f){return f.id === id;})); |
| 97 |
if (!original) return; |
| 98 |
var next = 'item-' + token + '-' + i; |
| 99 |
while (existing.has(next)) next += '-copy'; |
| 100 |
existing.add(next); ids[id] = next; |
| 101 |
var field = copy(original); field.id = next; |
| 102 |
if (operation === 'add') { |
| 103 |
if (field.type === 'image') { field.image = Object.assign({}, field.image, {src:'',alt:'',id:0}); delete field.image.srcset; delete field.image.sizes; field.value = ''; } |
| 104 |
else if (field.type === 'url') field.value = ''; |
| 105 |
else if (field.type === 'text' || field.type === 'richtext') field.value = ''; |
| 106 |
} |
| 107 |
fields.push(field); |
| 108 |
}); |
| 109 |
var elementIds = {}; |
| 110 |
function scanIds(n) { |
| 111 |
if (typeof (n.attrs || {}).id === 'string') elementIds[n.attrs.id] = n.attrs.id + '-item-' + token; |
| 112 |
(n.children || []).forEach(scanIds); |
| 113 |
} |
| 114 |
scanIds(clone); |
| 115 |
function remap(value, name) { |
| 116 |
if (value && typeof value === 'object') { |
| 117 |
if (ids[value.field]) value.field = ids[value.field]; |
| 118 |
Object.keys(value).forEach(function (k) { value[k] = remap(value[k], k); }); |
| 119 |
} else if (typeof value === 'string') { |
| 120 |
if (name === 'id' && elementIds[value]) return elementIds[value]; |
| 121 |
if (['href','xlink:href','fill','stroke','clip-path','mask','for','aria-controls','aria-labelledby','aria-describedby'].indexOf(name) >= 0) { |
| 122 |
Object.keys(elementIds).forEach(function (id) { |
| 123 |
value = value.replace('url(#'+id+')','url(#'+elementIds[id]+')'); |
| 124 |
if (value === '#'+id) value = '#'+elementIds[id]; |
| 125 |
if (['for','aria-controls','aria-labelledby','aria-describedby'].indexOf(name)>=0) value=value.split(/\s+/).map(function(v){return v===id?elementIds[id]:v;}).join(' '); |
| 126 |
}); |
| 127 |
} |
| 128 |
} |
| 129 |
return value; |
| 130 |
} |
| 131 |
remap(clone,''); clone.attrs = clone.attrs || {}; clone.attrs['data-bkbg-item-id'] = token; |
| 132 |
// Preserve source style classes, while stripping runtime state from copied items. |
| 133 |
clone.attrs.class = classes(clone).filter(function (c) { return !/^swiper-slide-(active|prev|next|duplicate)$/.test(c); }).join(' '); |
| 134 |
track.children.splice(operation === 'add' ? track.children.length : at+1,0,clone); |
| 135 |
} else return null; |
| 136 |
return {tree:tree,fields:fields,collectionTemplates:templates}; |
| 137 |
} |
| 138 |
var api = {groups:groups,change:change,references:references}; |
| 139 |
if (typeof module !== 'undefined' && module.exports) module.exports = api; |
| 140 |
else scope.bkbgTemplateCollections = api; |
| 141 |
})(typeof window !== 'undefined' ? window : this); |
| 142 |
|