PageSourceSearch

https://mnwebfest.org/_modules/mnwf/ui/festival_tools.js

js mnwebfest.org collected 2026-10-02 18:17:36 UTC 31,025 bytes, 805 lines download raw bytes

1import {locale} from "../../imaginary/util/i18n.js";
2import {Selection} from "../model/selection.js";
3import {Venue} from "../model/venue.js";
4import {Form} from "../../imaginary/ui/forms.js";
5import {UITools} from "../../imaginary/ui/tools.js";
6import {Strings} from "../../imaginary/util/strings.js";
7import {StreamingPlatform} from "../../imaginary/media/streaming_platforms.js";
8import {Submission} from "../model/submission.js";
9import {SubmissionCategory} from "../model/submission_category.js";
10import {Edition} from "../model/edition.js";
11import {Site} from "../../imaginary/site.js";
12import {Format} from "../../imaginary/ui/format.js";
13import {MembershipTier} from "../ecommerce/tiers.js";
14import {PriceGroup, PricePoints} from "../ecommerce/pricing.js";
15import {MerchandiseGroup} from "../ecommerce/merchandise_group.js";
16import {DataTools} from "../../imaginary/util/data.js";
17import {Member} from "../ecommerce/members.js";
18import {Money} from "../../imaginary/util/money.js";
19import {authentication} from "../../imaginary/sys/idm.js";
20import {Ticket} from "../ecommerce/ticket.js";
21
22
23export class FestivalUITools {
24    static async getEventTitle(editionId, eventId, eventType) {
25        const nodes = document.querySelectorAll('.data-node .event-data');
26
27        for( let i=0; i< nodes.length; i++ ) {
28            if( nodes[i].getAttribute('data-event-id') === eventId ) {
29                return nodes[i].getAttribute('data-title');
30            }
31        }
32        console.warn('No matching event was found in cached events, resorting to an API call which will fail on the public site');
33        let event = null;
34
35        if( eventType === 'block' ) {
36            const m = await import ('/_modules/mnwf/model/screening_block.js');
37
38            event = await m.ScreeningBlock.screeningBlock(editionId, eventId);
39        }
40        else if( eventType === 'stream' ) {
41            const m = await import ('/_modules/mnwf/model/live_stream.js');
42
43            event = await m.LiveStream.liveStream(editionId, eventId);
44        }
45        else if( eventType === 'live' ) {
46            const m = await import ('/_modules/mnwf/model/inperson.js');
47
48            event = await m.InPersonEvent.inPersonEvent(editionId, eventId);
49        }
50        if( !event ) {
51            return '';
52        }
53        return locale.translate(event.title);
54    }
55
56    static async describeEntitlements(ticket, entitlements, constraints) {
57        let blockConstraints, streamConstraints, liveConstraints;
58        let unlimitedBlocks, unlimitedStreams, unlimitedLive;
59        let blocks, streams, live;
60
61        if( DataTools.isEmpty(entitlements['blocks']) ) {
62            unlimitedBlocks = false;
63            blocks = [];
64            blockConstraints = null;
65        }
66        else {
67            const e = entitlements['blocks'];
68
69            if( !e.includes('*') ) {
70                unlimitedBlocks = false;
71                blocks = [];
72                for( let i=0; i<e.length; i++ ) {
73                    blocks.push(await FestivalUITools.getEventTitle(ticket.editionId, e[i], 'block'));
74                }
75            }
76            else {
77                unlimitedBlocks = true;
78                blocks = null;
79            }
80
81            const c = constraints['blocks'] || {};
82            const onDemandCount = c['max_on_demand_items'] || -1;
83            const onDemandReplays = c['max_on_demand_replays'] || -1;
84            const count = c['max_items'] || -1;
85            const replays = c['max_replays'] || -1;
86
87            if( onDemandCount !== -1 || onDemandReplays !== -1 || count !== -1 || replays !== -1 ) {
88                let txt;
89
90                unlimitedBlocks = false;
91                if( count !== -1 ) {
92                    blockConstraints = locale.translate('public', 'TXT_Constraint_Count', locale.pluralize('public', 'PLURAL_screening_block', count));
93                }
94                if( onDemandCount === -1 ) {
95                    txt = locale.translate('public', 'TXT_Has_On_Demand')
96                }
97                else if( onDemandCount === 0 ) {
98                    txt = locale.translate('public', 'TXT_No_On_Demand');
99                }
100                else {
101                    txt = locale.translate('public', 'TXT_On_Demand_Count', locale.pluralize('public', 'PLURAL_show', onDemandCount));
102                }
103                if( !blockConstraints ) {
104                    blockConstraints = txt;
105                }
106                else {
107                    blockConstraints = blockConstraints + ' ' + txt;
108                }
109                if( onDemandCount !== 0 && onDemandReplays !== -1 ) {
110                    if( onDemandReplays === 0 ) {
111                        blockConstraints = blockConstraints + ' ' + locale.translate('public', 'TXT_No_On_Demand_Replays');
112                    }
113                    else {
114                        blockConstraints = blockConstraints + ' ' + locale.translate('public', 'TXT_On_Demand_Replays', locale.pluralize('public', 'PLURAL_time', onDemandReplays));
115                    }
116                }
117                if( replays === 0 ) {
118                    blockConstraints = blockConstraints + ' ' + locale.translate('public', 'TXT_No_Replays');
119                }
120                else if( replays > 0 ) {
121                    blockConstraints = blockConstraints + ' ' + locale.translate('public', 'TXT_Replays', locale.pluralize('public', 'PLURAL_time', replays));
122                }
123            }
124        }
125        if( DataTools.isEmpty(entitlements['streams']) ) {
126            streamConstraints = null;
127            unlimitedStreams = false;
128            streams = [];
129        }
130        else {
131            const e = entitlements['streams'];
132
133            if( !e.includes('*') ) {
134                unlimitedStreams = false;
135                streams = [];
136                for( let i=0; i<e.length; i++ ) {
137                    streams.push(await FestivalUITools.getEventTitle(ticket.editionId, e[i], 'stream'));
138                }
139            }
140            else {
141                unlimitedStreams = true;
142                streams = null;
143            }
144
145            const c = constraints['streams'] || {};
146            const count = c['max_items'] || -1;
147            const replays = c['max_replays'] || -1;
148
149            if( count !== -1 || replays !== -1 ) {
150                let txt = null;
151
152                unlimitedStreams = false;
153                if( count !== -1 ) {
154                    streamConstraints = locale.translate('public', 'TXT_Constraint_Count', locale.pluralize('public', 'PLURAL_live_stream', count));
155                }
156                if( replays === 0 ) {
157                    txt = locale.translate('public', 'TXT_No_Stream_Replays');
158                }
159                else if( replays > 0 ) {
160                    txt = locale.translate('public', 'TXT_Stream_Replays', locale.pluralize('public', 'PLURAL_time', replays));
161                }
162                if( txt ) {
163                    if( !streamConstraints ) {
164                        streamConstraints = txt;
165                    }
166                    else {
167                        streamConstraints = streamConstraints + ' ' + txt;
168                    }
169                }
170            }
171        }
172        if( DataTools.isEmpty(entitlements['live']) ) {
173            liveConstraints = null;
174            unlimitedLive = false;
175            live = [];
176        }
177        else {
178            const e = entitlements['live'];
179
180            if( !e.includes('*') ) {
181                unlimitedLive = false;
182                live = [];
183                for( let i=0; i<e.length; i++ ) {
184                    live.push(await FestivalUITools.getEventTitle(ticket.editionId, e[i], 'live'));
185                }
186            }
187            else {
188                unlimitedLive = true;
189                live = null;
190            }
191
192            const c = constraints['live'] || {};
193            const count = c['max_items'] || -1;
194
195            if( count !== -1 ) {
196                unlimitedLive = false;
197                liveConstraints = locale.translate('public', 'TXT_Attend_Constraint_Count', locale.pluralize('public', 'PLURAL_in_person_event', count));
198            }
199        }
200        let description = null;
201
202        if( unlimitedBlocks && unlimitedStreams && unlimitedLive ) {
203            description = locale.translate('public', 'TXT_Unlimited_Access');
204        }
205        else if( unlimitedBlocks ) {
206            if( unlimitedStreams ) {
207                description = locale.translate('public', 'TXT_Unlimited_Block_And_Streams_Access');
208            }
209            else if( unlimitedLive ) {
210                description = locale.translate('public', 'TXT_Unlimited_Block_And_Live_Access');
211            }
212            else {
213                description = locale.translate('public', 'TXT_Unlimited_Block_Access');
214            }
215        }
216        else if( unlimitedStreams ) {
217            if( unlimitedLive ) {
218                description = locale.translate('public', 'TXT_Unlimited_Streams_And_Live_Access');
219            }
220            else {
221                description = locale.translate('public', 'TXT_Unlimited_Streams_Access');
222            }
223        }
224        else if( unlimitedLive ) {
225            description = locale.translate('public', 'TXT_Unlimited_Live_Access');
226        }
227        if( (blocks?.length ?? 0) > 0 ) {
228            let txt;
229
230            if( !blockConstraints ) {
231                txt = locale.translate('public', 'TXT_Blocks_Access_No_Constraints', locale.enumerateList(...blocks));
232            }
233            else {
234                txt = locale.translate('public', 'TXT_Blocks_Access', locale.enumerateList(...blocks), blockConstraints);
235            }
236            if( description ) {
237                description = description + ' ' + txt;
238            }
239            else {
240                description = txt;
241            }
242        }
243        else if( blockConstraints ) {
244            const txt = locale.translate('public', 'TXT_All_Block_Access', blockConstraints);
245
246            if( description ) {
247                description = description + ' ' + txt;
248            }
249            else {
250                description = txt;
251            }
252        }
253        if( (streams?.length ?? 0) > 0 ) {
254            let txt;
255
256            if( !streamConstraints ) {
257                txt = locale.translate('public', 'TXT_Streams_Access_No_Constraints', locale.enumerateList(...streams));
258            }
259            else {
260                txt = locale.translate('public', 'TXT_Streams_Access', locale.enumerateList(...streams), streamConstraints);
261            }
262            if( description ) {
263                description = description + ' ' + txt;
264            }
265            else {
266                description = txt;
267            }
268        }
269        else if( streamConstraints ) {
270            const txt = locale.translate('public', 'TXT_All_Stream_Access', streamConstraints);
271
272            if( description ) {
273                description = description + ' ' + txt;
274            }
275            else {
276                description = txt;
277            }
278        }
279        if( (live?.length ?? 0) > 0 ) {
280            let txt
281
282            if( !liveConstraints ) {
283                txt = locale.translate('public', 'TXT_Live_Access_No_Constraints', locale.enumerateList(...live));
284            }
285            else {
286                txt = locale.translate('public', 'TXT_Live_Access', locale.enumerateList(...live), liveConstraints);
287            }
288            if( description ) {
289                description = description + ' ' + txt;
290            }
291            else {
292                description = txt;
293            }
294        }
295        else if( liveConstraints ) {
296            const txt = locale.translate('public', 'TXT_All_Live_Access', liveConstraints);
297
298            if( description ) {
299                description = description + ' ' + txt;
300            }
301            else {
302                description = txt;
303            }
304        }
305        return description || '';
306    }
307
308
309    static cachedTier(ranking=null, tierId=null) {
310        let node = null;
311
312        if( tierId ) {
313            node = document.querySelector(`.data-node .membership-tier-data[data-membership-tier-id="${tierId}"]`);
314        }
315        else {
316            if( !ranking ) {
317                ranking = 0;
318            }
319            node = document.querySelector(`.data-node .membership-tier-data[data-ranking="${ranking}"]`);
320        }
321        if( !node ) {
322            return null;
323        }
324        let tier = {'id': node.getAttribute('data-membership-tier-id')};
325        const pricing = node.querySelector('.pricing-data');
326        const prices = pricing?.querySelectorAll('.price-point-data') ?? [];
327
328        tier['ranking'] = parseInt(node.getAttribute('data-ranking') ?? '0');
329        tier['sell_memberships'] = (node.getAttribute('data-for-sale') ?? 'false').toLowerCase() === 'true';
330        tier['canonical_name'] = node.getAttribute('data-canonical-name');
331        tier['name'] = node.getAttribute('data-name');
332        tier['description'] = node.querySelector('.data-tier-description').innerText.trim();
333        tier['product_id'] = node.querySelector('.product-data')?.getAttribute('data-product-id');
334        tier['pricing_id'] = pricing?.getAttribute('data-price-group-id');
335        tier['prices'] = [];
336        prices.forEach( (price) => {
337            let s = {};
338
339            s['tier_id'] = price.getAttribute('data-subscriber-level-id');
340            s['ranking'] = price.getAttribute('data-subscriber-level-ranking');
341            s['currency'] = price.getAttribute('data-currency');
342            s['amount'] = price.getAttribute('data-amount');
343            tier['prices'].push(s);
344        });
345        return tier;
346    }
347
348    static async localizePrices() {
349        const member = authentication.authenticated ? await Member.me() : null;
350
351        UITools.localizeCurrencyIcons();
352        UITools.configureNodes('.price', async(node) => {
353            const currency = node.getAttribute('data-currency');
354            const amount = node.getAttribute('data-amount');
355
356            if( !DataTools.isEmpty(amount) ) {
357                const money = await Money.money(currency || Site.defaultCurrency, parseInt(amount));
358
359                node.innerText = Format.formatMoney(money);
360            }
361        }).finally();
362
363        const myTier = FestivalUITools.cachedTier(null, member?.tierId)
364
365        UITools.configureNodes('.pricing', async(pricingNode) => {
366            const prices = pricingNode.querySelectorAll('.price');
367            let best = null;
368
369            for( let i = 0; i <prices.length; i++ ) {
370                const ranking = prices[i].getAttribute('data-price-ranking');
371
372                if( !best ) {
373                    best = {
374                        'index': i,
375                        'ranking': (DataTools.isEmpty(ranking) ? 0 : parseInt(ranking))
376                    };
377                }
378                else {
379                    const current = (DataTools.isEmpty(ranking) ? 0 : parseInt(ranking));
380
381                    if( current <= myTier['ranking'] && current > best['ranking'] ) {
382                        best = {
383                            'index': i,
384                            'ranking': current
385                        }
386                    }
387                }
388            }
389            if( best ) {
390                for( let i = 0; i < prices.length; i++ ) {
391                    if( i === best['index'] ) {
392                        prices[i].style.display = 'inline-flex';
393                        prices[i].classList.add('my-price');
394                    }
395                    else {
396                        prices[i].style.display = 'none';
397                        prices[i].classList.remove('my-price');
398                    }
399                }
400            }
401            pricingNode.setAttribute('data-localized', 'true');
402        }).finally();
403        await UITools.localizeMoney();
404    }
405
406    static matchesEventType(eventType, typeList) {
407        if( !typeList ) {
408            return false;
409        }
410        for( let i=0; i<typeList.length; i++ ) {
411            if( eventType.equals(typeList[i]) ) {
412                return true;
413            }
414        }
415        return false;
416    }
417
418    static async populatePriceGroups(form, key) {
419        const mg = await MerchandiseGroup.merchandiseGroupForKey(key);
420        let options = [];
421
422        if( mg ) {
423            const groups = await PriceGroup.priceGroups(mg.merchandiseGroupId);
424            const level = await MembershipTier.defaultTier();
425            const currency = Site.defaultCurrency;
426
427            for( let i = 0; i < groups.length; i++ ) {
428                const prices = await PricePoints.pricePoints(groups[i].priceGroupId);
429
430                if( prices ) {
431                    const price = await prices.getPrice(level.membershipTierId, currency);
432
433                    if( price ) {
434                        const label = groups[i].name + (price ? (' - ' + Format.formatMoney(price)) : '');
435
436                        options.push({
437                            'value': groups[i].priceGroupId,
438                            'label': label,
439                            'inputs': [groups[i].name]
440                        });
441                    }
442                }
443            }
444        }
445        form.setOptions(options, 'pricing');
446    }
447
448    /**
449     * Populates the list of known streaming platforms into ITAS fields and select fields.
450     * @param form {Form} the imaginary form object in which the fields to be populated exist
451     * @param emptyLabel {string|null} the label for the empty value if one is allowed
452     * @param emptyValue {string|null} the value to represent an empty value
453     * @returns {Promise<void>} this is an asynchronous method that returns no value
454     */
455    static async populatePlatforms(form,  emptyLabel, emptyValue='-') {
456        const allPlatforms = await StreamingPlatform.platforms();
457        let platforms = [];
458        let webSite = null;
459        let other = null;
460
461        for( let i=0; i<allPlatforms.length; i++ ) {
462            if( allPlatforms[i].name === 'Other' ) {
463                other = allPlatforms[i];
464            }
465            else if( allPlatforms[i].name === 'Web Site') {
466                webSite = allPlatforms[i];
467            }
468            else {
469                platforms.push(allPlatforms[i]);
470            }
471        }
472        platforms = platforms.sort( (a, b) => {
473            return a.name.localeCompare(b.name);
474        });
475        if( webSite ) {
476            platforms.push(webSite);
477        }
478        if( other ) {
479            platforms.push(other);
480        }
481        const options = platforms.map((platform) => {
482            let name = platform.name;
483
484            if( name === 'Other' ) {
485                name = locale.translate('creators', 'OPT_Other_Platform');
486            }
487            else if( name === 'Web Site' ) {
488                name = locale.translate('creators', 'OPT_Project_Web_Site');
489            }
490            return {
491                'value': platform.platformId,
492                'label': name,
493                'inputs': [name.toLowerCase(), name]
494            }
495        });
496        form.setOptions(options, 'platform', emptyLabel, emptyLabel ? emptyValue : null);
497    }
498
499    static async populateSelections(form, edition=null, emptyLabel=null, emptyValue=null, selections=null) {
500        if( !selections ) {
501            let editionId;
502
503            if( !edition && !window.application.selectedEdition ) {
504                editionId = (await Edition.current)?.editionId;
505            }
506            else {
507                editionId = (edition ?? window.application.selectedEdition).editionId
508            }
509            selections = await Selection.selections(editionId);
510        }
511        const options = selections.sort( (a,b ) => { return a.sortingTitle.localeCompare(b.sortingTitle); } ).map( (selection) => {
512            let inputs = [];
513
514            for( let lc in selection.title ) {
515                if( selection.title.hasOwnProperty(lc) ) {
516                    inputs.push(selection.title[lc].toLowerCase());
517                    inputs.push(Strings.canonicalName(selection.title[lc], ' '));
518                }
519            }
520            return {
521                'value': selection.selectionId,
522                'label': locale.translate(selection.title),
523                'inputs': inputs
524            };
525        });
526        if( form ) {
527            form.setOptions(options, 'selection', emptyLabel, emptyValue);
528        }
529        else {
530            Form.populate(options, 'selection', 'select', 'itas', 'many', 'required', emptyValue);
531        }
532    }
533
534    /*
535    static async populateSubmissions(form, edition=null, emptyLabel=null, emptyValue=null, submissions=null) {
536        if( !submissions ) {
537            submissions = await Submission.submissions((edition ?? window.application.selectedEdition).editionId);
538        }
539        const options = submissions.sort( (a,b ) => { return a.sortingTitle.localeCompare(b.sortingTitle); } ).map( (submission) => {
540            let inputs = [];
541
542            for( let lc in submission.title ) {
543                if( submission.title.hasOwnProperty(lc) ) {
544                    inputs.push(submission.title[lc].toLowerCase());
545                    inputs.push(Strings.canonicalName(submission.title[lc], ' '));
546                }
547            }
548            return {
549                'value': submission.submissionId,
550                'label': locale.translate(submission.title),
551                'inputs': inputs
552            };
553        });
554        if( form ) {
555            form.setOptions(options, 'submission', emptyLabel, emptyValue);
556        }
557        else {
558            Form.populate(options, 'submission', 'select', 'itas', 'many', 'required', emptyValue);
559        }
560    }
561*/
562
563    static async populateSubmissionCategories(form, edition=null, emptyLabel=null, emptyValue=null, categories=null) {
564        if( !categories ) {
565            categories = await SubmissionCategory.categories((edition ?? window.application.selectedEdition).editionId);
566        }
567        const options = categories.sort( (a,b ) => { return a.sortingTitle.localeCompare(b.sortingTitle); } ).map( (category) => {
568            let inputs = [category.name.toLowerCase(), Strings.canonicalName(category.name, ' ')];
569
570            (category.externalNames ?? []).forEach( (name) => {
571                inputs.push(name.toLowerCase());
572                inputs.push(Strings.canonicalName(name));
573            });
574            return {
575                'value': category.categoryId,
576                'label': category.name,
577                'inputs': inputs
578            };
579        });
580        if( form ) {
581            form.setOptions(options, 'submission-category', emptyLabel, emptyValue);
582        }
583        else {
584            Form.populate(options, 'submission-category', 'select', 'itas', 'many', 'required', emptyValue);
585        }
586    }
587
588    /**
589     * Populates all select-ticket-product and itas-ticket-product widgets in a form to enable people to select ticket
590     * products.
591     * @param form {Form} the imaginary form object for the form whose fields are to be populated
592     * @param groupIds {string} the group partition to which the tickets belong
593     * @returns {Promise<void>} this is an asynchronous method with no return value
594     */
595    static async populateTicketProducts(form, ...groupIds) {
596        const loaders = groupIds.map( (gid) => Ticket.tickets(gid) );
597        let options = [];
598
599        for( let i=0; i<loaders.length; i++ ) {
600            const tickets = await loaders[i];
601            const productsPromises = tickets.map( (ticket) => ticket.product );
602
603            for( let j = 0; j < tickets.length; j++ ) {
604                const product = await productsPromises[j];
605                const inputs = [Strings.canonicalName(locale.translate(tickets[j].title), ' ')];
606
607                options.push({
608                    'label': locale.translate(tickets[j].title),
609                    'value': product.productId,
610                    'inputs': inputs
611                });
612            }
613        }
614        form.setOptions(options, 'ticket-product');
615    }
616
617    static async populateMembershipTiers(form, useRankingAsValue=false) {
618        const levels = (await MembershipTier.membershipTiers()).sort( (a, b) => {
619            if( a.ranking === b.ranking ) {
620                return 0;
621            }
622            if( a.ranking > b.ranking ) {
623                return 1;
624            }
625            return -1;
626        });
627        const options = levels.map( (level) => {
628            const name = locale.translate(level.name);
629
630            return {
631                'value': useRankingAsValue ? ('' + level.ranking) : level.membershipTierId,
632                'label': `${level.ranking}. ${name}`,
633                'inputs': name
634            }
635        });
636        form.setOptions(options, 'subscriber-level');
637        form.setOptions(options, 'membership-tier');
638    }
639
640    static async populateUserTypes(form) {
641        const creators = locale.translate('festival', 'L_User_Creators');
642        const judges = locale.translate('festival', 'L_User_Judges');
643        const staff = locale.translate('festival', 'L_User_Staff');
644
645        form.setOptions([
646            {'label': creators, 'value': 'creators', 'inputs': ['creators', Strings.searchableString(creators)]},
647            {'label': judges, 'value': 'jury', 'inputs': ['judges', Strings.searchableString(judges)]},
648            {'label': staff, 'value': 'TEAM', 'inputs': ['staff', Strings.searchableString(staff)]}
649        ], 'customer-role');
650    }
651
652    static async populateInPersonEvents(select, allowNone=false, ...excludeTypes) {
653        const m = await import('/_modules/mnwf/model/inperson.js');
654        const events = (await m.InPersonEvent.inPersonEvents(window.application.selectedEdition.editionId)).sort( (a, b) => a.sortingTitle.localeCompare(b.sortingTitle) );
655
656        UITools.clearNode(select);
657        if( allowNone ) {
658            const option = document.createElement('option');
659
660            option.value = '-';
661            option.innerText = '-';
662            select.appendChild(option);
663        }
664        for( let i=0; i<events.length; i++ ) {
665            if( !FestivalUITools.matchesEventType(events[i].eventType, excludeTypes) ) {
666                const option = document.createElement('option');
667
668                option.value = events[i].eventId;
669                option.innerText = locale.translate(events[i].title) + ' (' + events[i].start['date'] + ')';
670                select.appendChild(option);
671            }
672        }
673    }
674
675    static async populateRooms(select, venueId, roomId) {
676        const venue = await Venue.venue(venueId);
677        const rooms = (await venue.rooms).filter( (r) => r.active );
678        let hasRoom = false;
679        let option;
680
681        if( roomId === '-' ) {
682            roomId = null;
683        }
684        UITools.clearNode(select);
685        option = document.createElement('option');
686        option.value = '-';
687        option.innerText = '-';
688        select.appendChild(option);
689        if( rooms && rooms.length > 0 ) {
690            for( let i=0; i< rooms.length; i++ ) {
691                if( roomId === rooms[i].venueId ) {
692                    hasRoom = true;
693                }
694                option = document.createElement('option');
695                option.value = rooms[i].venueId;
696                option.innerText = rooms[i].name;
697                select.appendChild(option);
698            }
699        }
700        if( hasRoom ) {
701            select.value = roomId ?? '-';
702        }
703        else {
704            select.value = '-';
705        }
706        return rooms;
707    }
708
709    static async populateVenues() {
710        const venues = (await Venue.venues()).filter( (v) => !v.parentVenueId && v.active );
711
712        let options = venues.sort( (a, b) => a.name.localeCompare(b.name)).map((venue) => {
713            return {
714                'value': venue.venueId,
715                'label': venue.name,
716                'inputs': [ venue.name ]
717            };
718        });
719        Form.populate(options, 'venue');
720    }
721
722    static reflectRequirements(form, requirements) {
723        const fields = form.form.querySelectorAll('.tabbed-form-field');
724
725        for( let field in requirements ) {
726            if( requirements.hasOwnProperty(field) ) {
727                const requirement = requirements[field];
728
729                for( let i=0; i<fields.length; i++ ) {
730                    const label = fields[i].querySelector('.label-text');
731
732                    if( label && label.getAttribute('data-label-for') === field ) {
733                        if( label.parentElement.classList.contains('label') ) {
734                            if( requirement.required ) {
735                                label.parentElement.classList.add('required');
736                            }
737                            else {
738                                label.parentElement.classList.remove('required');
739                            }
740                        }
741                        if( requirement.hidden ) {
742                            fields[i].style.display = 'none';
743                        }
744                        else {
745                            fields[i].style.display = 'flex';
746                        }
747                    }
748                }
749            }
750        }
751    }
752
753    static sortCards(columns, cardSelector, sortingAttribute='data-sorting-title', sortType='text') {
754        let cards = Array.from(document.querySelectorAll(cardSelector));
755
756        cards = cards.sort( (a, b) => {
757            if( a.style.display === 'none' ) {
758                if( b.style.display !== 'none') {
759                    return 1;
760                }
761            }
762            else if( b.style.display === 'none' ) {
763                return -1;
764            }
765
766            const valueOne = a.getAttribute(sortingAttribute);
767            const valueTwo = b.getAttribute(sortingAttribute);
768
769            if( sortType === 'number' ) {
770                const one = parseInt(valueOne);
771                const two = parseInt(valueTwo);
772
773                if( one > two ) {
774                    return 1;
775                }
776                else if( two > one ) {
777                    return -1;
778                }
779            }
780            else {
781                return valueOne?.localeCompare(valueTwo) ?? -1;
782            }
783            const titleOne = a.getAttribute('data-sorting-title') ?? '';
784            const titleTwo = b.getAttribute('data-sorting-title') ?? '';
785
786            return titleOne.localeCompare(titleTwo);
787        });
788
789        cards = cards.filter( (a) => a.style.display !== 'none' );
790        const singleColumn = window.screen.width <= 720;
791        let column = 1;
792
793        for( let i=0; i< cards.length; i++ ) {
794            columns[column-1].appendChild(cards[i]);
795            if( !singleColumn ) {
796                if( column === columns.length ) {
797                    column = 1;
798                }
799                else {
800                    column = column + 1;
801                }
802            }
803        }
804    }
805}

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.