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.