PageSourceSearch

https://liubov-nosova.com/local-repertoire.js?v=15

js liubov-nosova.com collected 2026-09-25 11:37:01 UTC 15,246 bytes, 379 lines download raw bytes

1(function () {
2  'use strict';
3
4  var ru = location.pathname === '/ru' || location.pathname.startsWith('/ru/');
5
6  function normalizedLetter(value) {
7    return String(value || '')
8      .trim()
9      .charAt(0)
10      .normalize('NFD')
11      .replace(/[\u0300-\u036f]/g, '')
12      .toLocaleUpperCase(ru ? 'ru-RU' : 'en-US');
13  }
14
15  function closeExpanded(record) {
16    record.querySelectorAll('.t585__trigger-button[aria-expanded="true"]').forEach(function (button) {
17      button.click();
18    });
19  }
20
21  function splitComposerName(value) {
22    var parts = String(value || '').split(',');
23    return {
24      surname: (parts.shift() || '').trim(),
25      givenName: parts.join(',').trim()
26    };
27  }
28
29  function composerInitials(value) {
30    return String(value || '')
31      .trim()
32      .split(/\s+/)
33      .filter(Boolean)
34      .map(function (part) {
35        return part.split('-').map(function (namePart) {
36          return namePart ? Array.from(namePart)[0].toLocaleUpperCase(ru ? 'ru-RU' : 'en-US') + '.' : '';
37        }).join('-');
38      })
39      .join(' ');
40  }
41
42  function structureEntry(accordion, title) {
43    var titleNode = accordion.querySelector('.t585__title');
44    var textWrapper = accordion.querySelector('.t585__textwrapper');
45    var text = accordion.querySelector('.t585__text');
46    var name = splitComposerName(title);
47
48    if (titleNode) {
49      titleNode.textContent = '';
50
51      var surname = document.createElement('span');
52      surname.className = 'ln-repertoire-composer__surname';
53      surname.textContent = name.surname + (name.givenName ? ' ' + composerInitials(name.givenName) : '');
54      titleNode.appendChild(surname);
55    }
56
57    if (textWrapper && text && !textWrapper.querySelector('.ln-repertoire-programme__label')) {
58      var label = document.createElement('div');
59      label.className = 'ln-repertoire-programme__label';
60      label.textContent = ru ? 'ПРОГРАММА' : 'PROGRAMME';
61      textWrapper.insertBefore(label, text);
62    }
63  }
64
65  function removeCategorySwitch() {
66    document.querySelectorAll('.r[data-record-type="1363"]').forEach(function (record) {
67      var text = record.textContent || '';
68      var isEnglishSwitch = /orchestral/i.test(text) && /operas/i.test(text);
69      var isRussianSwitch = /оркестров/i.test(text) && /оперн/i.test(text);
70      if (isEnglishSwitch || isRussianSwitch) record.remove();
71    });
72  }
73
74  function removeContactBlock() {
75    ['rec1919870581', 'rec1919870961', 'rec1919872111', 'rec1919957721'].forEach(function (id) {
76      document.getElementById(id)?.remove();
77    });
78    document.querySelectorAll('.ln-contact').forEach(function (section) {
79      section.remove();
80    });
81  }
82
83  function accordionTitle(accordion) {
84    return accordion.querySelector('.t585__title')?.textContent.trim() || '';
85  }
86
87  function findAccordion(record, composer) {
88    return Array.from(record.querySelectorAll('.t585__accordion')).find(function (accordion) {
89      return accordionTitle(accordion) === composer;
90    });
91  }
92
93  function setAccordionWorks(accordion, works) {
94    var list = accordion.querySelector('.t585__text ul');
95    if (!list) return;
96    list.textContent = '';
97    works.forEach(function (work) {
98      var item = document.createElement('li');
99      item.dataset.list = 'bullet';
100      item.textContent = work;
101      list.appendChild(item);
102    });
103  }
104
105  function bindLocalAccordion(accordion) {
106    var button = accordion.querySelector('.t585__trigger-button');
107    var header = accordion.querySelector('.t585__header');
108    var content = accordion.querySelector('.t585__content');
109    if (!button || !header || !content) return;
110    button.addEventListener('click', function () {
111      var expanded = button.getAttribute('aria-expanded') === 'true';
112      button.setAttribute('aria-expanded', String(!expanded));
113      header.classList.toggle('t585__opened', !expanded);
114      content.hidden = expanded;
115      content.style.display = expanded ? 'none' : 'block';
116      content.style.maxHeight = expanded ? '' : content.scrollHeight + 'px';
117    });
118  }
119
120  function buildColumn(template, composer, works, uid) {
121    var column = template.closest('.t-col').cloneNode(true);
122    var accordion = column.querySelector('.t585__accordion');
123    var title = accordion.querySelector('.t585__title');
124    var button = accordion.querySelector('.t585__trigger-button');
125    var content = accordion.querySelector('.t585__content');
126    var text = accordion.querySelector('.t585__text');
127
128    title.textContent = composer;
129    title.setAttribute('field', 'ln_title_' + uid);
130    text.setAttribute('field', 'ln_descr_' + uid);
131    button.setAttribute('aria-controls', uid);
132    button.setAttribute('aria-expanded', 'false');
133    content.id = uid;
134    content.hidden = true;
135    content.style.display = 'none';
136    setAccordionWorks(accordion, works || []);
137    bindLocalAccordion(accordion);
138    return column;
139  }
140
141  function addAccordion(record, section, update, updateIndex) {
142    var before = update.insertBefore ? findAccordion(record, update.insertBefore) : null;
143    var template = before || record.querySelector('.t585__accordion');
144    if (!template) return null;
145
146    var uid = 'ln-repertoire-' + (ru ? 'ru' : 'en') + '-' + section + '-' + updateIndex;
147    var column = buildColumn(template, update.composer, update.works, uid);
148
149    var beforeColumn = before?.closest('.t-col');
150    record.querySelector('.t-container').insertBefore(column, beforeColumn || null);
151    return column.querySelector('.t585__accordion');
152  }
153
154  function applyRepertoireUpdates(record, section) {
155    var dataNode = document.getElementById('ln-repertoire-updates');
156    if (!dataNode) return;
157
158    var updates;
159    try {
160      updates = (JSON.parse(dataNode.textContent)[ru ? 'ru' : 'en'] || []).filter(function (update) {
161        return (update.section || 'orchestral') === section;
162      });
163    } catch (error) {
164      return;
165    }
166
167    updates.forEach(function (update, updateIndex) {
168      var accordion = findAccordion(record, update.composer)
169        || (update.works ? addAccordion(record, section, update, updateIndex) : null);
170      if (!accordion) return;
171
172      var works = Array.from(accordion.querySelectorAll('.t585__text li')).map(function (item) {
173        return item.textContent.trim();
174      });
175
176      Object.entries(update.replace || {}).forEach(function (entry) {
177        var index = works.indexOf(entry[0]);
178        if (index < 0) return;
179        var replacement = Array.isArray(entry[1]) ? entry[1] : [entry[1]];
180        works.splice.apply(works, [index, 1].concat(replacement));
181      });
182
183      Object.entries(update.appendAfter || {}).forEach(function (entry) {
184        var index = works.indexOf(entry[0]);
185        var additions = Array.isArray(entry[1]) ? entry[1] : [entry[1]];
186        additions.forEach(function (work, offset) {
187          if (!works.includes(work)) works.splice(index < 0 ? works.length : index + 1 + offset, 0, work);
188        });
189      });
190
191      (update.append || []).forEach(function (work) {
192        if (!works.includes(work)) works.push(work);
193      });
194      setAccordionWorks(accordion, works);
195    });
196  }
197
198  // Раздел «Балеты» в зеркале Тильды отсутствует: на сайте есть только
199  // оркестровый и оперный блоки. Поэтому третий блок собирается из копии
200  // оперного — так он получает ту же вёрстку, те же брейкпоинты и тот же
201  // заголовок-зероблок, а не приблизительное повторение руками.
202  function cloneTildaRecord(source, newRecordId) {
203    var oldRecordId = source.id.replace('rec', '');
204    var clone = source.cloneNode(true);
205
206    (function rewrite(node) {
207      Array.from(node.attributes || []).forEach(function (attribute) {
208        if (attribute.value.includes(oldRecordId)) {
209          node.setAttribute(attribute.name, attribute.value.split(oldRecordId).join(newRecordId));
210        }
211      });
212      if (node.tagName === 'STYLE') {
213        node.textContent = node.textContent.split(oldRecordId).join(newRecordId);
214      }
215      Array.from(node.children).forEach(rewrite);
216    }(clone));
217
218    // Внутренние идентификаторы зероблока (molecule-…) в номер записи не входят,
219    // поэтому в копии они повторились бы. Переименовываем их вместе со ссылками
220    // на них в стилях блока.
221    var styles = Array.from(clone.querySelectorAll('style'));
222    clone.querySelectorAll('[id]').forEach(function (node) {
223      if (!document.getElementById(node.id)) return;
224      var renamed = node.id + '-' + newRecordId;
225      styles.forEach(function (style) {
226        style.textContent = style.textContent.split('#' + node.id).join('#' + renamed);
227      });
228      node.id = renamed;
229    });
230
231    // Скрипты Тильды в копии не выполнятся, а зероблок инициализируем вручную.
232    clone.querySelectorAll('script').forEach(function (script) { script.remove(); });
233    return clone;
234  }
235
236  var lastRecordId = 1900000000;
237
238  function nextRecordId() {
239    do { lastRecordId += 1; } while (document.getElementById('rec' + lastRecordId));
240    return String(lastRecordId);
241  }
242
243  function initZeroBlock(recordId) {
244    if (typeof window.t_onFuncLoad !== 'function') return;
245    window.t_onFuncLoad('t396_initialScale', function () { window.t396_initialScale(recordId); });
246    window.t_onFuncLoad('t396_init', function () { window.t396_init(recordId); });
247  }
248
249  function createBalletsSection(operasRecord) {
250    var dataNode = document.getElementById('ln-repertoire-updates');
251    if (!dataNode || !operasRecord) return null;
252
253    var section;
254    try {
255      section = (JSON.parse(dataNode.textContent).ballets || {})[ru ? 'ru' : 'en'];
256    } catch (error) {
257      return null;
258    }
259    if (!section || !Array.isArray(section.entries) || !section.entries.length) return null;
260
261    var operasHeading = operasRecord.previousElementSibling;
262    var template = operasRecord.querySelector('.t585__accordion');
263    if (!template || !operasHeading || operasHeading.dataset.recordType !== '396') return null;
264
265    var headingId = nextRecordId();
266    var heading = cloneTildaRecord(operasHeading, headingId);
267    var headingTitle = Array.from(heading.querySelectorAll('.tn-atom')).find(function (atom) {
268      return atom.textContent.trim();
269    });
270    if (headingTitle) headingTitle.textContent = section.heading;
271
272    var record = cloneTildaRecord(operasRecord, nextRecordId());
273    delete record.dataset.localRepertoire;
274    var container = record.querySelector('.t-container');
275    container.textContent = '';
276    section.entries.forEach(function (entry, index) {
277      var uid = 'ln-repertoire-ballet-' + (ru ? 'ru' : 'en') + '-' + index;
278      container.appendChild(buildColumn(template, entry.composer, entry.works, uid));
279    });
280
281    operasRecord.parentNode.insertBefore(heading, operasRecord.nextSibling);
282    heading.parentNode.insertBefore(record, heading.nextSibling);
283    initZeroBlock(headingId);
284    return record;
285  }
286
287  function createDownloadSection(record) {
288    var section = document.createElement('section');
289    section.className = 'ln-repertoire-download-section';
290    section.innerHTML = '<div class="ln-repertoire-download-section__inner">'
291      + '<a class="ln-repertoire-filter__download" href="/downloads/liubov-nosova-repertoire-'
292      + (ru ? 'ru' : 'en') + '.pdf" download>'
293      + '<span>' + (ru ? 'СКАЧАТЬ ПОЛНЫЙ РЕПЕРТУАР (PDF)' : 'DOWNLOAD FULL REPERTOIRE (PDF)') + '</span>'
294      + '<span class="ln-repertoire-filter__download-icon" aria-hidden="true">'
295      + '<span class="ln-repertoire-filter__download-icon-arrow"></span></span>'
296      + '</a></div>';
297
298    var categoryHeading = record.previousElementSibling;
299    record.parentNode.insertBefore(section, categoryHeading || record);
300  }
301
302  function enhance(record, recordIndex) {
303    if (record.dataset.localRepertoire === 'ready') return;
304
305    var entries = Array.from(record.querySelectorAll('.t585__accordion')).map(function (accordion) {
306      var title = accordion.querySelector('.t585__title')?.textContent.trim() || '';
307      structureEntry(accordion, title);
308      return {
309        item: accordion.closest('.t-col'),
310        letter: normalizedLetter(title)
311      };
312    }).filter(function (entry) {
313      return entry.item && entry.letter;
314    });
315    if (!entries.length) return;
316
317    var letters = Array.from(new Set(entries.map(function (entry) { return entry.letter; })));
318    var filter = document.createElement('section');
319    filter.className = 'ln-repertoire-filter';
320    filter.setAttribute('aria-label', ru ? 'Фильтр композиторов по алфавиту' : 'Filter composers by letter');
321    filter.innerHTML = '<div class="ln-repertoire-filter__inner">'
322      + '<div class="ln-repertoire-filter__alphabet" role="group"></div>'
323      + '</div>';
324
325    var alphabet = filter.querySelector('.ln-repertoire-filter__alphabet');
326    alphabet.innerHTML = letters.map(function (letter) {
327      return '<button class="ln-repertoire-filter__button" type="button" data-letter="'
328        + letter + '" aria-pressed="false">' + letter + '</button>';
329    }).join('');
330
331    record.insertBefore(filter, record.firstChild);
332    record.classList.add('ln-repertoire-record');
333    record.dataset.localRepertoire = 'ready';
334
335    if (recordIndex === 0) createDownloadSection(record);
336
337    entries.forEach(function (entry) {
338      entry.item.dataset.repertoireLetter = entry.letter;
339    });
340
341    function select(letter) {
342      closeExpanded(record);
343      alphabet.querySelectorAll('button').forEach(function (button) {
344        button.setAttribute('aria-pressed', String(button.dataset.letter === letter));
345      });
346      entries.forEach(function (entry) {
347        entry.item.classList.toggle('ln-repertoire-record__hidden', entry.letter !== letter);
348      });
349    }
350
351    alphabet.addEventListener('click', function (event) {
352      var button = event.target.closest('[data-letter]');
353      if (!button) return;
354      select(button.dataset.letter);
355    });
356
357    filter.id = 'ln-repertoire-filter-' + recordIndex;
358    select(letters[0]);
359  }
360
361  function init() {
362    removeCategorySwitch();
363    removeContactBlock();
364    var records = Array.from(document.querySelectorAll('.r[data-record-type="585"]'));
365    // Копию для «Балетов» снимаем до правок и оформления оперного блока,
366    // чтобы в неё не попали ни фильтр по алфавиту, ни чужие аккордеоны.
367    var ballets = createBalletsSection(records[1]);
368
369    records.forEach(function (record, recordIndex) {
370      applyRepertoireUpdates(record, recordIndex === 0 ? 'orchestral' : 'operas');
371      enhance(record, recordIndex);
372    });
373
374    if (ballets) enhance(ballets, records.length);
375  }
376
377  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
378  else init();
379}());

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.