@kerusiweb/angular 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
1
2
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { input, output, computed, ChangeDetectionStrategy, Component, inject, ElementRef, model, signal, effect } from '@angular/core';
|
|
3
|
-
import { DEFAULT_KERUSI_COLORS, seatFill, seatTextFill, cssVar, seatBodyPath, seatOccupantVariant, seatSelectedFrame, seatCoreFill, seatWashFill, seatOccupantPath, seatOccupantStroke, occupantFillFor, occupantStrokeFor, elementStyle, elementFill, elementTextFill, screenPath, readableOn, formatMoney, DEFAULT_SEAT_ARIA_STRINGS, errorsOf, KerusiValidationError, validateDocumentSet, expireHolds, buildRenderModel, resolveMapLocale, isRtlLocale, resolveColors, computeSectionLayout, buildNavigationGraph, seatAriaLabel, summarizeSelection, toggleSeatSelection, disallowedAnnouncement, DEFAULT_SEQUENCE_READER, heldSeats, applyStateDeltaOrdered } from '@kerusiweb/core';
|
|
3
|
+
import { input, output, computed, ChangeDetectionStrategy, Component, inject, ElementRef, model, signal, viewChild, effect, untracked } from '@angular/core';
|
|
4
|
+
import { DEFAULT_KERUSI_COLORS, seatFill, seatTextFill, cssVar, seatBodyPath, seatOccupantVariant, seatSelectedFrame, seatCoreFill, seatWashFill, seatOccupantPath, seatOccupantStroke, occupantFillFor, occupantStrokeFor, elementStyle, elementFill, elementTextFill, screenPath, readableOn, formatMoney, DEFAULT_SEAT_ARIA_STRINGS, DEFAULT_MAX_ZOOM, FIT_VIEWPORT, IDLE_GESTURE, errorsOf, KerusiValidationError, viewportsEqual, validateDocumentSet, expireHolds, buildRenderModel, resolveMapLocale, isRtlLocale, resolveColors, zoomLimits, resolveControlStrings, controlStyle, viewportTransform, viewportTouchAction, computeSectionLayout, buildNavigationGraph, seatAriaLabel, summarizeSelection, toggleSeatSelection, disallowedAnnouncement, fitViewport, stepViewport, clampViewport, revealRect, reduceGesture, wheelZoomFactor, zoomViewportBy, zoomViewportTo, isTextEntryTarget, keyboardViewport, DEFAULT_SEQUENCE_READER, heldSeats, applyStateDeltaOrdered } from '@kerusiweb/core';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Draws one Kerusi `Section` as a single `<svg>`.
|
|
@@ -375,6 +376,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
|
|
|
375
376
|
* ```html
|
|
376
377
|
* <kerusi-seatmap [map]="map" [state]="state" [(selection)]="picked" />
|
|
377
378
|
* ```
|
|
379
|
+
*
|
|
380
|
+
* Two opt-in features sit on top, both off by default so an existing map is
|
|
381
|
+
* unchanged: `[zoomable]` (pinch, wheel and drag, keys `+` `-` `0`, buttons)
|
|
382
|
+
* and `[maximizable]` (a full-screen button). Their arithmetic is in
|
|
383
|
+
* `@kerusiweb/core`; this class supplies the listeners and the signals.
|
|
378
384
|
*/
|
|
379
385
|
class KerusiSeatmapComponent {
|
|
380
386
|
host = inject((ElementRef));
|
|
@@ -456,6 +462,28 @@ class KerusiSeatmapComponent {
|
|
|
456
462
|
...(ngDevMode ? [{ debugName: "expireHolds" }] : /* istanbul ignore next */ []));
|
|
457
463
|
expiryIntervalMs = input(1000, /* @ts-ignore */
|
|
458
464
|
...(ngDevMode ? [{ debugName: "expiryIntervalMs" }] : /* istanbul ignore next */ []));
|
|
465
|
+
// --- zoom, pan and maximize (both opt in) ---------------------------------
|
|
466
|
+
/**
|
|
467
|
+
* Pinch, Ctrl/Cmd + wheel and drag to zoom and pan, `+` `-` `0` on the keyboard,
|
|
468
|
+
* and on-screen buttons. A tap still selects at every zoom level.
|
|
469
|
+
*/
|
|
470
|
+
zoomable = input(false, /* @ts-ignore */
|
|
471
|
+
...(ngDevMode ? [{ debugName: "zoomable" }] : /* istanbul ignore next */ []));
|
|
472
|
+
/** The zoom ceiling, relative to fit-to-container. The floor is always fit. */
|
|
473
|
+
maxZoom = input(DEFAULT_MAX_ZOOM, /* @ts-ignore */
|
|
474
|
+
...(ngDevMode ? [{ debugName: "maxZoom" }] : /* istanbul ignore next */ []));
|
|
475
|
+
/**
|
|
476
|
+
* The built-in zoom buttons. Turn this off to supply your own, anywhere on the
|
|
477
|
+
* page, and call `zoomIn()`, `zoomOut()` and `resetZoom()` on the component.
|
|
478
|
+
*/
|
|
479
|
+
showZoomControls = input(true, /* @ts-ignore */
|
|
480
|
+
...(ngDevMode ? [{ debugName: "showZoomControls" }] : /* istanbul ignore next */ []));
|
|
481
|
+
/** A button that maximizes the map to fill the screen. */
|
|
482
|
+
maximizable = input(false, /* @ts-ignore */
|
|
483
|
+
...(ngDevMode ? [{ debugName: "maximizable" }] : /* istanbul ignore next */ []));
|
|
484
|
+
/** Overrides the built-in English names of the zoom and maximize controls. */
|
|
485
|
+
controlStrings = input({}, /* @ts-ignore */
|
|
486
|
+
...(ngDevMode ? [{ debugName: "controlStrings" }] : /* istanbul ignore next */ []));
|
|
459
487
|
// --- validation -----------------------------------------------------------
|
|
460
488
|
validate = input('collect', /* @ts-ignore */
|
|
461
489
|
...(ngDevMode ? [{ debugName: "validate" }] : /* istanbul ignore next */ []));
|
|
@@ -466,6 +494,9 @@ class KerusiSeatmapComponent {
|
|
|
466
494
|
seatFocus = output();
|
|
467
495
|
/** Emitted whenever the documents change, in `collect` mode. */
|
|
468
496
|
validationIssues = output();
|
|
497
|
+
/** The zoom and pan, whenever they change. For a host that persists or shows them. */
|
|
498
|
+
viewportChange = output();
|
|
499
|
+
maximizedChange = output();
|
|
469
500
|
// --- internal state -------------------------------------------------------
|
|
470
501
|
/**
|
|
471
502
|
* The seat holding each section's tab stop, keyed by section id.
|
|
@@ -481,6 +512,25 @@ class KerusiSeatmapComponent {
|
|
|
481
512
|
/** Bumped by the hold ticker to force the state to be re-derived. */
|
|
482
513
|
expiryTick = signal(0, /* @ts-ignore */
|
|
483
514
|
...(ngDevMode ? [{ debugName: "expiryTick" }] : /* istanbul ignore next */ []));
|
|
515
|
+
viewportState = signal(FIT_VIEWPORT, /* @ts-ignore */
|
|
516
|
+
...(ngDevMode ? [{ debugName: "viewportState" }] : /* istanbul ignore next */ []));
|
|
517
|
+
maximizedState = signal(false, /* @ts-ignore */
|
|
518
|
+
...(ngDevMode ? [{ debugName: "maximizedState" }] : /* istanbul ignore next */ []));
|
|
519
|
+
/** True for a discrete change (button, key, reveal), false while a finger or wheel drives. */
|
|
520
|
+
animated = signal(false, /* @ts-ignore */
|
|
521
|
+
...(ngDevMode ? [{ debugName: "animated" }] : /* istanbul ignore next */ []));
|
|
522
|
+
viewportEl = viewChild('viewport', /* @ts-ignore */
|
|
523
|
+
...(ngDevMode ? [{ debugName: "viewportEl" }] : /* istanbul ignore next */ []));
|
|
524
|
+
contentEl = viewChild('content', /* @ts-ignore */
|
|
525
|
+
...(ngDevMode ? [{ debugName: "contentEl" }] : /* istanbul ignore next */ []));
|
|
526
|
+
gesture = IDLE_GESTURE;
|
|
527
|
+
suppressClick = false;
|
|
528
|
+
gestureBase = null;
|
|
529
|
+
nativeFullscreen = false;
|
|
530
|
+
/** The current zoom and pan. Read-only: drive it with the methods below. */
|
|
531
|
+
viewport = this.viewportState.asReadonly();
|
|
532
|
+
/** Whether the map is currently maximized. */
|
|
533
|
+
maximized = this.maximizedState.asReadonly();
|
|
484
534
|
constructor() {
|
|
485
535
|
effect(() => {
|
|
486
536
|
const issues = this.issues();
|
|
@@ -520,6 +570,93 @@ class KerusiSeatmapComponent {
|
|
|
520
570
|
const handle = setInterval(() => this.expiryTick.update((n) => n + 1), this.expiryIntervalMs());
|
|
521
571
|
onCleanup(() => clearInterval(handle));
|
|
522
572
|
});
|
|
573
|
+
// Gesture listeners go on natively, not through the template: the wheel
|
|
574
|
+
// listener must be non-passive to `preventDefault` a page zoom, and the
|
|
575
|
+
// click listener must be in the capture phase to swallow the click that
|
|
576
|
+
// follows a drag before the seat sees it.
|
|
577
|
+
effect((onCleanup) => {
|
|
578
|
+
const el = this.viewportEl()?.nativeElement;
|
|
579
|
+
if (!el) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const listeners = [
|
|
583
|
+
['pointerdown', (e) => this.onPointerDown(e), false],
|
|
584
|
+
['pointermove', (e) => this.onPointerMove(e), false],
|
|
585
|
+
['pointerup', (e) => this.onPointerEnd(e, 'up'), false],
|
|
586
|
+
['pointercancel', (e) => this.onPointerEnd(e, 'cancel'), false],
|
|
587
|
+
['wheel', (e) => this.onWheel(e), { passive: false }],
|
|
588
|
+
['click', (e) => this.onClickCapture(e), true],
|
|
589
|
+
// Safari reports a trackpad pinch as gesture events, not Ctrl + wheel.
|
|
590
|
+
['gesturestart', (e) => this.onGestureStart(e), { passive: false }],
|
|
591
|
+
['gesturechange', (e) => this.onGestureChange(e), { passive: false }],
|
|
592
|
+
['gestureend', (e) => this.onGestureEnd(e), { passive: false }],
|
|
593
|
+
];
|
|
594
|
+
for (const [type, listener, options] of listeners) {
|
|
595
|
+
el.addEventListener(type, listener, options);
|
|
596
|
+
}
|
|
597
|
+
onCleanup(() => {
|
|
598
|
+
for (const [type, listener, options] of listeners) {
|
|
599
|
+
el.removeEventListener(type, listener, options);
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
// Re-clamp when the box or the ceiling changes: a rotated phone, a
|
|
604
|
+
// maximized map, a lowered `maxZoom`.
|
|
605
|
+
effect((onCleanup) => {
|
|
606
|
+
this.limits();
|
|
607
|
+
const view = this.viewportEl()?.nativeElement;
|
|
608
|
+
untracked(() => this.reclamp());
|
|
609
|
+
if (!view || typeof ResizeObserver === 'undefined') {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const observer = new ResizeObserver(() => this.reclamp());
|
|
613
|
+
observer.observe(view);
|
|
614
|
+
const content = this.contentEl()?.nativeElement;
|
|
615
|
+
if (content) {
|
|
616
|
+
observer.observe(content);
|
|
617
|
+
}
|
|
618
|
+
onCleanup(() => observer.disconnect());
|
|
619
|
+
});
|
|
620
|
+
// Switching `zoomable` or `maximizable` off puts the map back as it was.
|
|
621
|
+
effect(() => {
|
|
622
|
+
if (!this.zoomable() && !viewportsEqual(untracked(this.viewportState), FIT_VIEWPORT)) {
|
|
623
|
+
this.viewportState.set(FIT_VIEWPORT);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
effect(() => {
|
|
627
|
+
if (!this.maximizable() && untracked(this.maximizedState)) {
|
|
628
|
+
untracked(() => this.setMaximized(false));
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
// While maximized: freeze the page behind, and follow the browser leaving
|
|
632
|
+
// native full screen (Esc) so the two never disagree.
|
|
633
|
+
effect((onCleanup) => {
|
|
634
|
+
if (!this.maximizedState()) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const host = this.host.nativeElement;
|
|
638
|
+
const doc = host.ownerDocument;
|
|
639
|
+
const previousOverflow = doc.body.style.overflow;
|
|
640
|
+
doc.body.style.overflow = 'hidden';
|
|
641
|
+
const onFullscreenChange = () => {
|
|
642
|
+
if (doc.fullscreenElement === host) {
|
|
643
|
+
this.nativeFullscreen = true;
|
|
644
|
+
}
|
|
645
|
+
else if (this.nativeFullscreen) {
|
|
646
|
+
this.nativeFullscreen = false;
|
|
647
|
+
this.setMaximized(false);
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
doc.addEventListener('fullscreenchange', onFullscreenChange);
|
|
651
|
+
onCleanup(() => {
|
|
652
|
+
doc.body.style.overflow = previousOverflow;
|
|
653
|
+
doc.removeEventListener('fullscreenchange', onFullscreenChange);
|
|
654
|
+
this.nativeFullscreen = false;
|
|
655
|
+
if (doc.fullscreenElement === host) {
|
|
656
|
+
doc.exitFullscreen?.().catch(() => undefined);
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
});
|
|
523
660
|
}
|
|
524
661
|
// --- derived model --------------------------------------------------------
|
|
525
662
|
issues = computed(() => this.validate() === 'off'
|
|
@@ -555,6 +692,22 @@ class KerusiSeatmapComponent {
|
|
|
555
692
|
...(ngDevMode ? [{ debugName: "resolvedColors" }] : /* istanbul ignore next */ []));
|
|
556
693
|
selectionSet = computed(() => new Set(this.selection()), /* @ts-ignore */
|
|
557
694
|
...(ngDevMode ? [{ debugName: "selectionSet" }] : /* istanbul ignore next */ []));
|
|
695
|
+
limits = computed(() => zoomLimits(this.maxZoom()), /* @ts-ignore */
|
|
696
|
+
...(ngDevMode ? [{ debugName: "limits" }] : /* istanbul ignore next */ []));
|
|
697
|
+
strings = computed(() => resolveControlStrings(this.controlStrings()), /* @ts-ignore */
|
|
698
|
+
...(ngDevMode ? [{ debugName: "strings" }] : /* istanbul ignore next */ []));
|
|
699
|
+
controlColors = computed(() => controlStyle(this.colors()), /* @ts-ignore */
|
|
700
|
+
...(ngDevMode ? [{ debugName: "controlColors" }] : /* istanbul ignore next */ []));
|
|
701
|
+
transform = computed(() => viewportTransform(this.viewportState()), /* @ts-ignore */
|
|
702
|
+
...(ngDevMode ? [{ debugName: "transform" }] : /* istanbul ignore next */ []));
|
|
703
|
+
touchAction = computed(() => viewportTouchAction(this.viewportState(), this.maximizedState()), /* @ts-ignore */
|
|
704
|
+
...(ngDevMode ? [{ debugName: "touchAction" }] : /* istanbul ignore next */ []));
|
|
705
|
+
showToolbar = computed(() => (this.zoomable() && this.showZoomControls()) || this.maximizable(), /* @ts-ignore */
|
|
706
|
+
...(ngDevMode ? [{ debugName: "showToolbar" }] : /* istanbul ignore next */ []));
|
|
707
|
+
atMaxZoom = computed(() => this.viewportState().zoom >= this.limits().maxZoom - 1e-4, /* @ts-ignore */
|
|
708
|
+
...(ngDevMode ? [{ debugName: "atMaxZoom" }] : /* istanbul ignore next */ []));
|
|
709
|
+
atFit = computed(() => this.viewportState().zoom <= 1 + 1e-4, /* @ts-ignore */
|
|
710
|
+
...(ngDevMode ? [{ debugName: "atFit" }] : /* istanbul ignore next */ []));
|
|
558
711
|
/** Placed geometry per section, keyed by section id. */
|
|
559
712
|
layouts = computed(() => {
|
|
560
713
|
const overrides = this.sectionOverrides();
|
|
@@ -643,6 +796,9 @@ class KerusiSeatmapComponent {
|
|
|
643
796
|
}
|
|
644
797
|
this.setFocusedSeat(seat.sectionId, seatId);
|
|
645
798
|
this.seatFocus.emit(seat);
|
|
799
|
+
if (this.zoomable()) {
|
|
800
|
+
this.revealSeat(seatId);
|
|
801
|
+
}
|
|
646
802
|
}
|
|
647
803
|
/**
|
|
648
804
|
* Arrow keys walk the navigation graph, which orders seats by `col` in grid
|
|
@@ -714,19 +870,247 @@ class KerusiSeatmapComponent {
|
|
|
714
870
|
/**
|
|
715
871
|
* Moves the roving tab stop, then the DOM focus that follows it.
|
|
716
872
|
*
|
|
873
|
+
* When zoomable, the browser's own scroll-into-view is suppressed: the map
|
|
874
|
+
* moves by transform, so `onSeatFocused` pans it into view instead.
|
|
875
|
+
*/
|
|
876
|
+
moveFocusTo(sectionId, seatId) {
|
|
877
|
+
this.setFocusedSeat(sectionId, seatId);
|
|
878
|
+
const node = this.seatNode(seatId);
|
|
879
|
+
if (this.zoomable()) {
|
|
880
|
+
node?.focus?.({ preventScroll: true });
|
|
881
|
+
}
|
|
882
|
+
else {
|
|
883
|
+
node?.focus?.();
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
717
887
|
* Seat ids come from the document, so they cannot be interpolated into a
|
|
718
888
|
* selector unescaped — and `CSS.escape` is absent in jsdom and under SSR.
|
|
719
889
|
* Scanning the rendered nodes avoids both problems.
|
|
720
890
|
*/
|
|
721
|
-
|
|
722
|
-
this.setFocusedSeat(sectionId, seatId);
|
|
891
|
+
seatNode(seatId) {
|
|
723
892
|
const nodes = this.host.nativeElement.querySelectorAll('[data-seat-id]');
|
|
724
893
|
for (const node of Array.from(nodes)) {
|
|
725
894
|
if (node.dataset['seatId'] === seatId) {
|
|
726
|
-
node
|
|
895
|
+
return node;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
// --- zoom and pan ---------------------------------------------------------
|
|
901
|
+
/** Zooms in one step about the centre. No-op unless `zoomable`. */
|
|
902
|
+
zoomIn() {
|
|
903
|
+
this.stepZoom(1);
|
|
904
|
+
}
|
|
905
|
+
/** Zooms out one step about the centre. No-op unless `zoomable`. */
|
|
906
|
+
zoomOut() {
|
|
907
|
+
this.stepZoom(-1);
|
|
908
|
+
}
|
|
909
|
+
/** Back to fit-to-container: the map's original view. No-op unless `zoomable`. */
|
|
910
|
+
resetZoom() {
|
|
911
|
+
const measured = this.measure();
|
|
912
|
+
if (measured && this.zoomable()) {
|
|
913
|
+
this.commit(fitViewport(measured.content, measured.container, this.limits()), true);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
stepZoom(direction) {
|
|
917
|
+
const measured = this.measure();
|
|
918
|
+
if (measured && this.zoomable()) {
|
|
919
|
+
const next = stepViewport(this.viewportState(), direction, measured.content, measured.container, this.limits());
|
|
920
|
+
this.commit(next, true);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
/** Sizes in CSS pixels, read when needed: `offset*` ignores the zoom transform. */
|
|
924
|
+
measure() {
|
|
925
|
+
const view = this.viewportEl()?.nativeElement;
|
|
926
|
+
const content = this.contentEl()?.nativeElement;
|
|
927
|
+
if (!view || !content) {
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
return {
|
|
931
|
+
container: { width: view.clientWidth, height: view.clientHeight },
|
|
932
|
+
content: { width: content.offsetWidth, height: content.offsetHeight },
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
commit(next, animate) {
|
|
936
|
+
if (viewportsEqual(next, this.viewportState())) {
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
this.animated.set(animate);
|
|
940
|
+
this.viewportState.set(next);
|
|
941
|
+
this.viewportChange.emit(next);
|
|
942
|
+
}
|
|
943
|
+
reclamp() {
|
|
944
|
+
const measured = this.measure();
|
|
945
|
+
if (measured && this.zoomable()) {
|
|
946
|
+
const next = clampViewport(this.viewportState(), measured.content, measured.container, this.limits());
|
|
947
|
+
this.commit(next, false);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
/** A point in the viewport's own coordinates, which is what the core functions take. */
|
|
951
|
+
containerPoint(event) {
|
|
952
|
+
const box = this.viewportEl().nativeElement.getBoundingClientRect();
|
|
953
|
+
return { x: event.clientX - box.left, y: event.clientY - box.top };
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Pans a seat that took keyboard focus into view. A click or tap also
|
|
957
|
+
* focuses the seat, but should not move the map under the finger, so only a
|
|
958
|
+
* `:focus-visible` focus counts.
|
|
959
|
+
*/
|
|
960
|
+
revealSeat(seatId) {
|
|
961
|
+
const view = this.viewportEl()?.nativeElement;
|
|
962
|
+
const node = this.seatNode(seatId);
|
|
963
|
+
const measured = this.measure();
|
|
964
|
+
if (!view || !node || !measured || !focusIsVisible(node)) {
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
const seat = node.getBoundingClientRect();
|
|
968
|
+
const frame = view.getBoundingClientRect();
|
|
969
|
+
const next = revealRect(this.viewportState(), {
|
|
970
|
+
x: seat.left - frame.left,
|
|
971
|
+
y: seat.top - frame.top,
|
|
972
|
+
width: seat.width,
|
|
973
|
+
height: seat.height,
|
|
974
|
+
}, measured.content, measured.container, undefined, this.limits());
|
|
975
|
+
this.commit(next, true);
|
|
976
|
+
}
|
|
977
|
+
// --- pointer gestures -----------------------------------------------------
|
|
978
|
+
onPointerDown(event) {
|
|
979
|
+
if (event.pointerType === 'mouse' && event.button !== 0) {
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
this.suppressClick = false;
|
|
983
|
+
this.feedGesture({ type: 'down', id: event.pointerId, point: this.containerPoint(event) });
|
|
984
|
+
}
|
|
985
|
+
onPointerMove(event) {
|
|
986
|
+
if (this.gesture.ids.length > 0) {
|
|
987
|
+
this.feedGesture({ type: 'move', id: event.pointerId, point: this.containerPoint(event) });
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
onPointerEnd(event, type) {
|
|
991
|
+
this.feedGesture({ type, id: event.pointerId });
|
|
992
|
+
}
|
|
993
|
+
feedGesture(event) {
|
|
994
|
+
const measured = this.measure();
|
|
995
|
+
if (!measured) {
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
const result = reduceGesture(this.gesture, event, {
|
|
999
|
+
viewport: this.viewportState(),
|
|
1000
|
+
content: measured.content,
|
|
1001
|
+
container: measured.container,
|
|
1002
|
+
limits: this.limits(),
|
|
1003
|
+
});
|
|
1004
|
+
this.gesture = result.state;
|
|
1005
|
+
// Capture only once a tap has become a gesture: capturing on `down` would
|
|
1006
|
+
// retarget the tap's click away from the seat.
|
|
1007
|
+
if (result.capture) {
|
|
1008
|
+
const view = this.viewportEl()?.nativeElement;
|
|
1009
|
+
for (const id of result.state.ids) {
|
|
1010
|
+
try {
|
|
1011
|
+
view?.setPointerCapture(id);
|
|
1012
|
+
}
|
|
1013
|
+
catch {
|
|
1014
|
+
// The pointer already ended; nothing to capture.
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
if (result.viewport !== this.viewportState()) {
|
|
1019
|
+
this.commit(result.viewport, false);
|
|
1020
|
+
}
|
|
1021
|
+
if (result.suppressClick) {
|
|
1022
|
+
// The click a drag ends with arrives right after `pointerup`, before this timer.
|
|
1023
|
+
this.suppressClick = true;
|
|
1024
|
+
setTimeout(() => (this.suppressClick = false));
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
onClickCapture(event) {
|
|
1028
|
+
if (this.suppressClick) {
|
|
1029
|
+
this.suppressClick = false;
|
|
1030
|
+
event.stopPropagation();
|
|
1031
|
+
event.preventDefault();
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
onWheel(event) {
|
|
1035
|
+
const factor = wheelZoomFactor(event);
|
|
1036
|
+
const measured = this.measure();
|
|
1037
|
+
if (factor === null || !measured) {
|
|
1038
|
+
return; // A plain wheel is the page's to scroll.
|
|
1039
|
+
}
|
|
1040
|
+
event.preventDefault();
|
|
1041
|
+
const next = zoomViewportBy(this.viewportState(), factor, this.containerPoint(event), measured.content, measured.container, this.limits());
|
|
1042
|
+
this.commit(next, false);
|
|
1043
|
+
}
|
|
1044
|
+
onGestureStart(event) {
|
|
1045
|
+
event.preventDefault();
|
|
1046
|
+
this.gestureBase = this.viewportState();
|
|
1047
|
+
}
|
|
1048
|
+
onGestureChange(event) {
|
|
1049
|
+
event.preventDefault();
|
|
1050
|
+
const measured = this.measure();
|
|
1051
|
+
if (!this.gestureBase || !measured) {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
const next = zoomViewportTo(this.gestureBase, this.gestureBase.zoom * event.scale, this.containerPoint(event), measured.content, measured.container, this.limits());
|
|
1055
|
+
this.commit(next, false);
|
|
1056
|
+
}
|
|
1057
|
+
onGestureEnd(event) {
|
|
1058
|
+
event.preventDefault();
|
|
1059
|
+
this.gestureBase = null;
|
|
1060
|
+
}
|
|
1061
|
+
// --- keyboard on the map as a whole ----------------------------------------
|
|
1062
|
+
/**
|
|
1063
|
+
* `+` `-` `0` zoom, and arrows pan when the viewport itself has focus; Esc
|
|
1064
|
+
* leaves a maximized map. A seat's own handler runs first, so an Esc that
|
|
1065
|
+
* cleared the selection is already `defaultPrevented` and stays at that.
|
|
1066
|
+
*/
|
|
1067
|
+
onRootKeydown(event) {
|
|
1068
|
+
if (event.defaultPrevented || isTextEntryTarget(event.target)) {
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
if (this.zoomable()) {
|
|
1072
|
+
const measured = this.measure();
|
|
1073
|
+
const next = measured &&
|
|
1074
|
+
keyboardViewport(this.viewportState(), {
|
|
1075
|
+
key: event.key,
|
|
1076
|
+
ctrlKey: event.ctrlKey,
|
|
1077
|
+
metaKey: event.metaKey,
|
|
1078
|
+
altKey: event.altKey,
|
|
1079
|
+
viewportFocused: event.target === this.viewportEl()?.nativeElement,
|
|
1080
|
+
}, measured.content, measured.container, this.limits());
|
|
1081
|
+
if (next) {
|
|
1082
|
+
event.preventDefault();
|
|
1083
|
+
this.commit(next, true);
|
|
727
1084
|
return;
|
|
728
1085
|
}
|
|
729
1086
|
}
|
|
1087
|
+
if (event.key === 'Escape' && this.maximizedState()) {
|
|
1088
|
+
event.preventDefault();
|
|
1089
|
+
this.setMaximized(false);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
// --- maximize -------------------------------------------------------------
|
|
1093
|
+
/** Maximizes or restores the map. Maximizing needs `maximizable`. */
|
|
1094
|
+
toggleMaximized() {
|
|
1095
|
+
this.setMaximized(!this.maximizedState());
|
|
1096
|
+
}
|
|
1097
|
+
setMaximized(next) {
|
|
1098
|
+
if (next === this.maximizedState() || (next && !this.maximizable())) {
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
this.maximizedState.set(next);
|
|
1102
|
+
this.maximizedChange.emit(next);
|
|
1103
|
+
// Native full screen is an enhancement over the fixed-position layout, which
|
|
1104
|
+
// is all iPhone Safari offers. It is requested here, inside the click that
|
|
1105
|
+
// got us here, because browsers only grant it to a user gesture.
|
|
1106
|
+
const host = this.host.nativeElement;
|
|
1107
|
+
const doc = host.ownerDocument;
|
|
1108
|
+
if (next && !doc.fullscreenElement) {
|
|
1109
|
+
host.requestFullscreen?.()?.catch(() => undefined);
|
|
1110
|
+
}
|
|
1111
|
+
else if (!next && doc.fullscreenElement === host) {
|
|
1112
|
+
doc.exitFullscreen?.()?.catch(() => undefined);
|
|
1113
|
+
}
|
|
730
1114
|
}
|
|
731
1115
|
announceSelection(selection) {
|
|
732
1116
|
const strings = this.ariaStrings();
|
|
@@ -758,15 +1142,24 @@ class KerusiSeatmapComponent {
|
|
|
758
1142
|
return (this.map().sections ?? []).map((s) => s.id).filter((id) => !hidden.includes(id));
|
|
759
1143
|
}
|
|
760
1144
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: KerusiSeatmapComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
761
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: KerusiSeatmapComponent, isStandalone: true, selector: "kerusi-seatmap", inputs: { map: { classPropertyName: "map", publicName: "map", isSignal: true, isRequired: true, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, session: { classPropertyName: "session", publicName: "session", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, rtl: { classPropertyName: "rtl", publicName: "rtl", isSignal: true, isRequired: false, transformFunction: null }, ariaStrings: { classPropertyName: "ariaStrings", publicName: "ariaStrings", isSignal: true, isRequired: false, transformFunction: null }, colors: { classPropertyName: "colors", publicName: "colors", isSignal: true, isRequired: false, transformFunction: null }, typeColors: { classPropertyName: "typeColors", publicName: "typeColors", isSignal: true, isRequired: false, transformFunction: null }, seatSize: { classPropertyName: "seatSize", publicName: "seatSize", isSignal: true, isRequired: false, transformFunction: null }, seatGap: { classPropertyName: "seatGap", publicName: "seatGap", isSignal: true, isRequired: false, transformFunction: null }, freeformBasis: { classPropertyName: "freeformBasis", publicName: "freeformBasis", isSignal: true, isRequired: false, transformFunction: null }, unitScale: { classPropertyName: "unitScale", publicName: "unitScale", isSignal: true, isRequired: false, transformFunction: null }, showSectionLabels: { classPropertyName: "showSectionLabels", publicName: "showSectionLabels", isSignal: true, isRequired: false, transformFunction: null }, showLegend: { classPropertyName: "showLegend", publicName: "showLegend", isSignal: true, isRequired: false, transformFunction: null }, showLegendPrices: { classPropertyName: "showLegendPrices", publicName: "showLegendPrices", isSignal: true, isRequired: false, transformFunction: null }, sectionIds: { classPropertyName: "sectionIds", publicName: "sectionIds", isSignal: true, isRequired: false, transformFunction: null }, sectionOverrides: { classPropertyName: "sectionOverrides", publicName: "sectionOverrides", isSignal: true, isRequired: false, transformFunction: null }, selectableStatuses: { classPropertyName: "selectableStatuses", publicName: "selectableStatuses", isSignal: true, isRequired: false, transformFunction: null }, companionMode: { classPropertyName: "companionMode", publicName: "companionMode", isSignal: true, isRequired: false, transformFunction: null }, maxSelection: { classPropertyName: "maxSelection", publicName: "maxSelection", isSignal: true, isRequired: false, transformFunction: null }, seatSelectable: { classPropertyName: "seatSelectable", publicName: "seatSelectable", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, expireHolds: { classPropertyName: "expireHolds", publicName: "expireHolds", isSignal: true, isRequired: false, transformFunction: null }, expiryIntervalMs: { classPropertyName: "expiryIntervalMs", publicName: "expiryIntervalMs", isSignal: true, isRequired: false, transformFunction: null }, validate: { classPropertyName: "validate", publicName: "validate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selection: "selectionChange", seatSelect: "seatSelect", seatDeselect: "seatDeselect", seatDisallowed: "seatDisallowed", seatFocus: "seatFocus", validationIssues: "validationIssues" }, ngImport: i0, template: "<div\r\n class=\"kerusi-seatmap\"\r\n role=\"group\"\r\n [attr.aria-label]=\"mapLabel()\"\r\n [attr.dir]=\"isRtl() ? 'rtl' : null\"\r\n [attr.lang]=\"resolvedLocale()\"\r\n>\r\n @for (section of renderMap().sections; track section.id) {\r\n <section\r\n class=\"kerusi-seatmap__section kerusi-seatmap__section--{{ section.layoutMode }}\"\r\n role=\"group\"\r\n [attr.aria-labelledby]=\"labelFor(section) ? labelId(section) : null\"\r\n [attr.aria-label]=\"labelFor(section) ? null : section.id\"\r\n >\r\n @if (showSectionLabels() && labelFor(section)) {\r\n <h3 class=\"kerusi-section-label\" [id]=\"labelId(section)\">{{ labelFor(section) }}</h3>\r\n }\r\n\r\n <kerusi-section\r\n [section]=\"section\"\r\n [layout]=\"layoutFor(section)\"\r\n [selection]=\"selectionSet()\"\r\n [focusedSeatId]=\"focusedSeatIn(section)\"\r\n [ariaLabels]=\"ariaLabels()\"\r\n [colors]=\"resolvedColors()\"\r\n [typeColors]=\"typeColors()\"\r\n [unitScale]=\"unitScale()\"\r\n [interactive]=\"interactive()\"\r\n (seatActivate)=\"onSeatActivate($event)\"\r\n (seatFocused)=\"onSeatFocused($event)\"\r\n (seatKeydown)=\"onKeydown(section, $event)\"\r\n />\r\n </section>\r\n }\r\n\r\n @if (showLegend()) {\r\n <kerusi-legend\r\n [legend]=\"renderMap().legend\"\r\n [locale]=\"resolvedLocale()\"\r\n [colors]=\"resolvedColors()\"\r\n [typeColors]=\"typeColors()\"\r\n [showPrices]=\"showLegendPrices()\"\r\n />\r\n }\r\n\r\n <p class=\"kerusi-sr-only\" aria-live=\"polite\" aria-atomic=\"true\">{{ liveMessage() }}</p>\r\n</div>\r\n", styles: [":host{display:block}.kerusi-seatmap{display:flex;flex-direction:column;gap:1.25rem}.kerusi-seatmap__section{display:flex;flex-direction:column;gap:.4rem}.kerusi-section-label{margin:0;font-size:.85rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;opacity:.75}.kerusi-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "component", type: KerusiSectionComponent, selector: "kerusi-section", inputs: ["section", "layout", "selection", "focusedSeatId", "ariaLabels", "colors", "typeColors", "interactive", "unitScale"], outputs: ["seatActivate", "seatFocused", "seatKeydown"] }, { kind: "component", type: KerusiLegendComponent, selector: "kerusi-legend", inputs: ["legend", "locale", "colors", "typeColors", "showPrices", "hideUnusedTypes", "headingSeatTypes", "headingAvailability"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1145
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: KerusiSeatmapComponent, isStandalone: true, selector: "kerusi-seatmap", inputs: { map: { classPropertyName: "map", publicName: "map", isSignal: true, isRequired: true, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, session: { classPropertyName: "session", publicName: "session", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, rtl: { classPropertyName: "rtl", publicName: "rtl", isSignal: true, isRequired: false, transformFunction: null }, ariaStrings: { classPropertyName: "ariaStrings", publicName: "ariaStrings", isSignal: true, isRequired: false, transformFunction: null }, colors: { classPropertyName: "colors", publicName: "colors", isSignal: true, isRequired: false, transformFunction: null }, typeColors: { classPropertyName: "typeColors", publicName: "typeColors", isSignal: true, isRequired: false, transformFunction: null }, seatSize: { classPropertyName: "seatSize", publicName: "seatSize", isSignal: true, isRequired: false, transformFunction: null }, seatGap: { classPropertyName: "seatGap", publicName: "seatGap", isSignal: true, isRequired: false, transformFunction: null }, freeformBasis: { classPropertyName: "freeformBasis", publicName: "freeformBasis", isSignal: true, isRequired: false, transformFunction: null }, unitScale: { classPropertyName: "unitScale", publicName: "unitScale", isSignal: true, isRequired: false, transformFunction: null }, showSectionLabels: { classPropertyName: "showSectionLabels", publicName: "showSectionLabels", isSignal: true, isRequired: false, transformFunction: null }, showLegend: { classPropertyName: "showLegend", publicName: "showLegend", isSignal: true, isRequired: false, transformFunction: null }, showLegendPrices: { classPropertyName: "showLegendPrices", publicName: "showLegendPrices", isSignal: true, isRequired: false, transformFunction: null }, sectionIds: { classPropertyName: "sectionIds", publicName: "sectionIds", isSignal: true, isRequired: false, transformFunction: null }, sectionOverrides: { classPropertyName: "sectionOverrides", publicName: "sectionOverrides", isSignal: true, isRequired: false, transformFunction: null }, selectableStatuses: { classPropertyName: "selectableStatuses", publicName: "selectableStatuses", isSignal: true, isRequired: false, transformFunction: null }, companionMode: { classPropertyName: "companionMode", publicName: "companionMode", isSignal: true, isRequired: false, transformFunction: null }, maxSelection: { classPropertyName: "maxSelection", publicName: "maxSelection", isSignal: true, isRequired: false, transformFunction: null }, seatSelectable: { classPropertyName: "seatSelectable", publicName: "seatSelectable", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, expireHolds: { classPropertyName: "expireHolds", publicName: "expireHolds", isSignal: true, isRequired: false, transformFunction: null }, expiryIntervalMs: { classPropertyName: "expiryIntervalMs", publicName: "expiryIntervalMs", isSignal: true, isRequired: false, transformFunction: null }, zoomable: { classPropertyName: "zoomable", publicName: "zoomable", isSignal: true, isRequired: false, transformFunction: null }, maxZoom: { classPropertyName: "maxZoom", publicName: "maxZoom", isSignal: true, isRequired: false, transformFunction: null }, showZoomControls: { classPropertyName: "showZoomControls", publicName: "showZoomControls", isSignal: true, isRequired: false, transformFunction: null }, maximizable: { classPropertyName: "maximizable", publicName: "maximizable", isSignal: true, isRequired: false, transformFunction: null }, controlStrings: { classPropertyName: "controlStrings", publicName: "controlStrings", isSignal: true, isRequired: false, transformFunction: null }, validate: { classPropertyName: "validate", publicName: "validate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selection: "selectionChange", seatSelect: "seatSelect", seatDeselect: "seatDeselect", seatDisallowed: "seatDisallowed", seatFocus: "seatFocus", validationIssues: "validationIssues", viewportChange: "viewportChange", maximizedChange: "maximizedChange" }, viewQueries: [{ propertyName: "viewportEl", first: true, predicate: ["viewport"], descendants: true, isSignal: true }, { propertyName: "contentEl", first: true, predicate: ["content"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\r\n class=\"kerusi-seatmap\"\r\n [class.kerusi-seatmap--maximized]=\"maximized()\"\r\n role=\"group\"\r\n [attr.aria-label]=\"mapLabel()\"\r\n [attr.dir]=\"isRtl() ? 'rtl' : null\"\r\n [attr.lang]=\"resolvedLocale()\"\r\n [style.background]=\"maximized() ? controlColors().maximizedBackground : null\"\r\n [style.color]=\"maximized() ? controlColors().maximizedColor : null\"\r\n (keydown)=\"onRootKeydown($event)\"\r\n>\r\n @if (zoomable() || maximizable()) {\r\n <!-- Only when a zoom or maximize feature is on, so a plain map's markup is\r\n exactly what it was before they existed. -->\r\n <div class=\"kerusi-seatmap__stage\">\r\n @if (showToolbar()) {\r\n <div\r\n class=\"kerusi-toolbar\"\r\n role=\"toolbar\"\r\n [attr.aria-label]=\"strings().toolbar\"\r\n [style.--kerusi-focus-ring-input]=\"resolvedColors().focusRing\"\r\n >\r\n @if (zoomable() && showZoomControls()) {\r\n <button\r\n type=\"button\"\r\n class=\"kerusi-control kerusi-control--zoom-in\"\r\n [attr.aria-label]=\"strings().zoomIn\"\r\n [attr.aria-disabled]=\"atMaxZoom()\"\r\n [style.background]=\"controlColors().buttonBackground\"\r\n [style.color]=\"controlColors().buttonColor\"\r\n [style.border-color]=\"controlColors().buttonBorder\"\r\n (click)=\"zoomIn()\"\r\n >\r\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\r\n <path d=\"M12 5v14M5 12h14\" />\r\n </svg>\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"kerusi-control kerusi-control--zoom-out\"\r\n [attr.aria-label]=\"strings().zoomOut\"\r\n [attr.aria-disabled]=\"atFit()\"\r\n [style.background]=\"controlColors().buttonBackground\"\r\n [style.color]=\"controlColors().buttonColor\"\r\n [style.border-color]=\"controlColors().buttonBorder\"\r\n (click)=\"zoomOut()\"\r\n >\r\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\r\n <path d=\"M5 12h14\" />\r\n </svg>\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"kerusi-control kerusi-control--zoom-reset\"\r\n [attr.aria-label]=\"strings().zoomReset\"\r\n [attr.aria-disabled]=\"atFit()\"\r\n [style.background]=\"controlColors().buttonBackground\"\r\n [style.color]=\"controlColors().buttonColor\"\r\n [style.border-color]=\"controlColors().buttonBorder\"\r\n (click)=\"resetZoom()\"\r\n >\r\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\r\n <path d=\"M3 12a9 9 0 1 0 3-6.7M3 4v5h5\" />\r\n </svg>\r\n </button>\r\n }\r\n @if (maximizable()) {\r\n <button\r\n type=\"button\"\r\n class=\"kerusi-control kerusi-control--maximize\"\r\n [attr.aria-label]=\"maximized() ? strings().restore : strings().maximize\"\r\n [style.background]=\"controlColors().buttonBackground\"\r\n [style.color]=\"controlColors().buttonColor\"\r\n [style.border-color]=\"controlColors().buttonBorder\"\r\n (click)=\"toggleMaximized()\"\r\n >\r\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\r\n @if (maximized()) {\r\n <path d=\"M9 4v5H4M15 4v5h5M15 20v-5h5M9 20v-5H4\" />\r\n } @else {\r\n <path d=\"M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5\" />\r\n }\r\n </svg>\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n @if (zoomable()) {\r\n <div\r\n #viewport\r\n class=\"kerusi-viewport\"\r\n role=\"group\"\r\n tabindex=\"0\"\r\n [attr.aria-label]=\"strings().viewport\"\r\n [style.touch-action]=\"touchAction()\"\r\n [style.--kerusi-focus-ring-input]=\"resolvedColors().focusRing\"\r\n >\r\n <div\r\n #content\r\n class=\"kerusi-viewport__content\"\r\n [class.kerusi-viewport__content--animated]=\"animated()\"\r\n [style.transform]=\"transform()\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"sections\" />\r\n </div>\r\n </div>\r\n } @else {\r\n <ng-container [ngTemplateOutlet]=\"sections\" />\r\n }\r\n </div>\r\n } @else {\r\n <ng-container [ngTemplateOutlet]=\"sections\" />\r\n }\r\n\r\n @if (showLegend()) {\r\n <kerusi-legend\r\n [legend]=\"renderMap().legend\"\r\n [locale]=\"resolvedLocale()\"\r\n [colors]=\"resolvedColors()\"\r\n [typeColors]=\"typeColors()\"\r\n [showPrices]=\"showLegendPrices()\"\r\n />\r\n }\r\n\r\n <p class=\"kerusi-sr-only\" aria-live=\"polite\" aria-atomic=\"true\">{{ liveMessage() }}</p>\r\n</div>\r\n\r\n<ng-template #sections>\r\n @for (section of renderMap().sections; track section.id) {\r\n <section\r\n class=\"kerusi-seatmap__section kerusi-seatmap__section--{{ section.layoutMode }}\"\r\n role=\"group\"\r\n [attr.aria-labelledby]=\"labelFor(section) ? labelId(section) : null\"\r\n [attr.aria-label]=\"labelFor(section) ? null : section.id\"\r\n >\r\n @if (showSectionLabels() && labelFor(section)) {\r\n <h3 class=\"kerusi-section-label\" [id]=\"labelId(section)\">{{ labelFor(section) }}</h3>\r\n }\r\n\r\n <kerusi-section\r\n [section]=\"section\"\r\n [layout]=\"layoutFor(section)\"\r\n [selection]=\"selectionSet()\"\r\n [focusedSeatId]=\"focusedSeatIn(section)\"\r\n [ariaLabels]=\"ariaLabels()\"\r\n [colors]=\"resolvedColors()\"\r\n [typeColors]=\"typeColors()\"\r\n [unitScale]=\"unitScale()\"\r\n [interactive]=\"interactive()\"\r\n (seatActivate)=\"onSeatActivate($event)\"\r\n (seatFocused)=\"onSeatFocused($event)\"\r\n (seatKeydown)=\"onKeydown(section, $event)\"\r\n />\r\n </section>\r\n }\r\n</ng-template>\r\n", styles: [":host{display:block}.kerusi-seatmap{display:flex;flex-direction:column;gap:1.25rem}.kerusi-seatmap__section{display:flex;flex-direction:column;gap:.4rem}.kerusi-section-label{margin:0;font-size:.85rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;opacity:.75}.kerusi-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}.kerusi-seatmap__stage{display:flex;flex-direction:column;gap:1.25rem}.kerusi-toolbar{display:flex;gap:.35rem;align-self:flex-end;flex:none}.kerusi-control{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:2.75rem;height:2.75rem;padding:0;border:1px solid;border-radius:.4rem;font:inherit;cursor:pointer}.kerusi-control[aria-disabled=true]{opacity:.45;cursor:default}.kerusi-control svg{width:1.4rem;height:1.4rem;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;pointer-events:none}.kerusi-control:focus-visible,.kerusi-viewport:focus-visible{outline:2px solid var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));outline-offset:2px}.kerusi-viewport{position:relative;overflow:hidden;overflow:clip;user-select:none;-webkit-user-select:none}.kerusi-viewport__content{display:flex;flex-direction:column;gap:1.25rem;transform-origin:0 0}.kerusi-viewport__content--animated{transition:transform .16s ease-out}@media(prefers-reduced-motion:reduce){.kerusi-viewport__content--animated{transition:none}}.kerusi-seatmap--maximized{position:fixed;inset:0;z-index:var(--kerusi-maximized-z-index, 1000);box-sizing:border-box;padding:.75rem;gap:.5rem;overflow:hidden;overscroll-behavior:contain}.kerusi-seatmap--maximized .kerusi-seatmap__stage{flex:1 1 auto;min-height:0;gap:.5rem;overflow:auto}.kerusi-seatmap--maximized .kerusi-viewport{flex:1 1 auto;min-height:0}\n"], dependencies: [{ kind: "component", type: KerusiSectionComponent, selector: "kerusi-section", inputs: ["section", "layout", "selection", "focusedSeatId", "ariaLabels", "colors", "typeColors", "interactive", "unitScale"], outputs: ["seatActivate", "seatFocused", "seatKeydown"] }, { kind: "component", type: KerusiLegendComponent, selector: "kerusi-legend", inputs: ["legend", "locale", "colors", "typeColors", "showPrices", "hideUnusedTypes", "headingSeatTypes", "headingAvailability"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
762
1146
|
}
|
|
763
1147
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: KerusiSeatmapComponent, decorators: [{
|
|
764
1148
|
type: Component,
|
|
765
|
-
args: [{ selector: 'kerusi-seatmap', changeDetection: ChangeDetectionStrategy.OnPush, imports: [KerusiSectionComponent, KerusiLegendComponent], template: "<div\r\n class=\"kerusi-seatmap\"\r\n role=\"group\"\r\n [attr.aria-label]=\"mapLabel()\"\r\n [attr.dir]=\"isRtl() ? 'rtl' : null\"\r\n [attr.lang]=\"resolvedLocale()\"\r\n>\r\n @
|
|
766
|
-
}], ctorParameters: () => [], propDecorators: { map: [{ type: i0.Input, args: [{ isSignal: true, alias: "map", required: true }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], session: [{ type: i0.Input, args: [{ isSignal: true, alias: "session", required: false }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }, { type: i0.Output, args: ["selectionChange"] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], rtl: [{ type: i0.Input, args: [{ isSignal: true, alias: "rtl", required: false }] }], ariaStrings: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaStrings", required: false }] }], colors: [{ type: i0.Input, args: [{ isSignal: true, alias: "colors", required: false }] }], typeColors: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeColors", required: false }] }], seatSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatSize", required: false }] }], seatGap: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatGap", required: false }] }], freeformBasis: [{ type: i0.Input, args: [{ isSignal: true, alias: "freeformBasis", required: false }] }], unitScale: [{ type: i0.Input, args: [{ isSignal: true, alias: "unitScale", required: false }] }], showSectionLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSectionLabels", required: false }] }], showLegend: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLegend", required: false }] }], showLegendPrices: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLegendPrices", required: false }] }], sectionIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "sectionIds", required: false }] }], sectionOverrides: [{ type: i0.Input, args: [{ isSignal: true, alias: "sectionOverrides", required: false }] }], selectableStatuses: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectableStatuses", required: false }] }], companionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "companionMode", required: false }] }], maxSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSelection", required: false }] }], seatSelectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatSelectable", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], expireHolds: [{ type: i0.Input, args: [{ isSignal: true, alias: "expireHolds", required: false }] }], expiryIntervalMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "expiryIntervalMs", required: false }] }], validate: [{ type: i0.Input, args: [{ isSignal: true, alias: "validate", required: false }] }], seatSelect: [{ type: i0.Output, args: ["seatSelect"] }], seatDeselect: [{ type: i0.Output, args: ["seatDeselect"] }], seatDisallowed: [{ type: i0.Output, args: ["seatDisallowed"] }], seatFocus: [{ type: i0.Output, args: ["seatFocus"] }], validationIssues: [{ type: i0.Output, args: ["validationIssues"] }] } });
|
|
1149
|
+
args: [{ selector: 'kerusi-seatmap', changeDetection: ChangeDetectionStrategy.OnPush, imports: [KerusiSectionComponent, KerusiLegendComponent, NgTemplateOutlet], template: "<div\r\n class=\"kerusi-seatmap\"\r\n [class.kerusi-seatmap--maximized]=\"maximized()\"\r\n role=\"group\"\r\n [attr.aria-label]=\"mapLabel()\"\r\n [attr.dir]=\"isRtl() ? 'rtl' : null\"\r\n [attr.lang]=\"resolvedLocale()\"\r\n [style.background]=\"maximized() ? controlColors().maximizedBackground : null\"\r\n [style.color]=\"maximized() ? controlColors().maximizedColor : null\"\r\n (keydown)=\"onRootKeydown($event)\"\r\n>\r\n @if (zoomable() || maximizable()) {\r\n <!-- Only when a zoom or maximize feature is on, so a plain map's markup is\r\n exactly what it was before they existed. -->\r\n <div class=\"kerusi-seatmap__stage\">\r\n @if (showToolbar()) {\r\n <div\r\n class=\"kerusi-toolbar\"\r\n role=\"toolbar\"\r\n [attr.aria-label]=\"strings().toolbar\"\r\n [style.--kerusi-focus-ring-input]=\"resolvedColors().focusRing\"\r\n >\r\n @if (zoomable() && showZoomControls()) {\r\n <button\r\n type=\"button\"\r\n class=\"kerusi-control kerusi-control--zoom-in\"\r\n [attr.aria-label]=\"strings().zoomIn\"\r\n [attr.aria-disabled]=\"atMaxZoom()\"\r\n [style.background]=\"controlColors().buttonBackground\"\r\n [style.color]=\"controlColors().buttonColor\"\r\n [style.border-color]=\"controlColors().buttonBorder\"\r\n (click)=\"zoomIn()\"\r\n >\r\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\r\n <path d=\"M12 5v14M5 12h14\" />\r\n </svg>\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"kerusi-control kerusi-control--zoom-out\"\r\n [attr.aria-label]=\"strings().zoomOut\"\r\n [attr.aria-disabled]=\"atFit()\"\r\n [style.background]=\"controlColors().buttonBackground\"\r\n [style.color]=\"controlColors().buttonColor\"\r\n [style.border-color]=\"controlColors().buttonBorder\"\r\n (click)=\"zoomOut()\"\r\n >\r\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\r\n <path d=\"M5 12h14\" />\r\n </svg>\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"kerusi-control kerusi-control--zoom-reset\"\r\n [attr.aria-label]=\"strings().zoomReset\"\r\n [attr.aria-disabled]=\"atFit()\"\r\n [style.background]=\"controlColors().buttonBackground\"\r\n [style.color]=\"controlColors().buttonColor\"\r\n [style.border-color]=\"controlColors().buttonBorder\"\r\n (click)=\"resetZoom()\"\r\n >\r\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\r\n <path d=\"M3 12a9 9 0 1 0 3-6.7M3 4v5h5\" />\r\n </svg>\r\n </button>\r\n }\r\n @if (maximizable()) {\r\n <button\r\n type=\"button\"\r\n class=\"kerusi-control kerusi-control--maximize\"\r\n [attr.aria-label]=\"maximized() ? strings().restore : strings().maximize\"\r\n [style.background]=\"controlColors().buttonBackground\"\r\n [style.color]=\"controlColors().buttonColor\"\r\n [style.border-color]=\"controlColors().buttonBorder\"\r\n (click)=\"toggleMaximized()\"\r\n >\r\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" focusable=\"false\">\r\n @if (maximized()) {\r\n <path d=\"M9 4v5H4M15 4v5h5M15 20v-5h5M9 20v-5H4\" />\r\n } @else {\r\n <path d=\"M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5\" />\r\n }\r\n </svg>\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n @if (zoomable()) {\r\n <div\r\n #viewport\r\n class=\"kerusi-viewport\"\r\n role=\"group\"\r\n tabindex=\"0\"\r\n [attr.aria-label]=\"strings().viewport\"\r\n [style.touch-action]=\"touchAction()\"\r\n [style.--kerusi-focus-ring-input]=\"resolvedColors().focusRing\"\r\n >\r\n <div\r\n #content\r\n class=\"kerusi-viewport__content\"\r\n [class.kerusi-viewport__content--animated]=\"animated()\"\r\n [style.transform]=\"transform()\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"sections\" />\r\n </div>\r\n </div>\r\n } @else {\r\n <ng-container [ngTemplateOutlet]=\"sections\" />\r\n }\r\n </div>\r\n } @else {\r\n <ng-container [ngTemplateOutlet]=\"sections\" />\r\n }\r\n\r\n @if (showLegend()) {\r\n <kerusi-legend\r\n [legend]=\"renderMap().legend\"\r\n [locale]=\"resolvedLocale()\"\r\n [colors]=\"resolvedColors()\"\r\n [typeColors]=\"typeColors()\"\r\n [showPrices]=\"showLegendPrices()\"\r\n />\r\n }\r\n\r\n <p class=\"kerusi-sr-only\" aria-live=\"polite\" aria-atomic=\"true\">{{ liveMessage() }}</p>\r\n</div>\r\n\r\n<ng-template #sections>\r\n @for (section of renderMap().sections; track section.id) {\r\n <section\r\n class=\"kerusi-seatmap__section kerusi-seatmap__section--{{ section.layoutMode }}\"\r\n role=\"group\"\r\n [attr.aria-labelledby]=\"labelFor(section) ? labelId(section) : null\"\r\n [attr.aria-label]=\"labelFor(section) ? null : section.id\"\r\n >\r\n @if (showSectionLabels() && labelFor(section)) {\r\n <h3 class=\"kerusi-section-label\" [id]=\"labelId(section)\">{{ labelFor(section) }}</h3>\r\n }\r\n\r\n <kerusi-section\r\n [section]=\"section\"\r\n [layout]=\"layoutFor(section)\"\r\n [selection]=\"selectionSet()\"\r\n [focusedSeatId]=\"focusedSeatIn(section)\"\r\n [ariaLabels]=\"ariaLabels()\"\r\n [colors]=\"resolvedColors()\"\r\n [typeColors]=\"typeColors()\"\r\n [unitScale]=\"unitScale()\"\r\n [interactive]=\"interactive()\"\r\n (seatActivate)=\"onSeatActivate($event)\"\r\n (seatFocused)=\"onSeatFocused($event)\"\r\n (seatKeydown)=\"onKeydown(section, $event)\"\r\n />\r\n </section>\r\n }\r\n</ng-template>\r\n", styles: [":host{display:block}.kerusi-seatmap{display:flex;flex-direction:column;gap:1.25rem}.kerusi-seatmap__section{display:flex;flex-direction:column;gap:.4rem}.kerusi-section-label{margin:0;font-size:.85rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;opacity:.75}.kerusi-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}.kerusi-seatmap__stage{display:flex;flex-direction:column;gap:1.25rem}.kerusi-toolbar{display:flex;gap:.35rem;align-self:flex-end;flex:none}.kerusi-control{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:2.75rem;height:2.75rem;padding:0;border:1px solid;border-radius:.4rem;font:inherit;cursor:pointer}.kerusi-control[aria-disabled=true]{opacity:.45;cursor:default}.kerusi-control svg{width:1.4rem;height:1.4rem;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;pointer-events:none}.kerusi-control:focus-visible,.kerusi-viewport:focus-visible{outline:2px solid var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));outline-offset:2px}.kerusi-viewport{position:relative;overflow:hidden;overflow:clip;user-select:none;-webkit-user-select:none}.kerusi-viewport__content{display:flex;flex-direction:column;gap:1.25rem;transform-origin:0 0}.kerusi-viewport__content--animated{transition:transform .16s ease-out}@media(prefers-reduced-motion:reduce){.kerusi-viewport__content--animated{transition:none}}.kerusi-seatmap--maximized{position:fixed;inset:0;z-index:var(--kerusi-maximized-z-index, 1000);box-sizing:border-box;padding:.75rem;gap:.5rem;overflow:hidden;overscroll-behavior:contain}.kerusi-seatmap--maximized .kerusi-seatmap__stage{flex:1 1 auto;min-height:0;gap:.5rem;overflow:auto}.kerusi-seatmap--maximized .kerusi-viewport{flex:1 1 auto;min-height:0}\n"] }]
|
|
1150
|
+
}], ctorParameters: () => [], propDecorators: { map: [{ type: i0.Input, args: [{ isSignal: true, alias: "map", required: true }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], session: [{ type: i0.Input, args: [{ isSignal: true, alias: "session", required: false }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }, { type: i0.Output, args: ["selectionChange"] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], rtl: [{ type: i0.Input, args: [{ isSignal: true, alias: "rtl", required: false }] }], ariaStrings: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaStrings", required: false }] }], colors: [{ type: i0.Input, args: [{ isSignal: true, alias: "colors", required: false }] }], typeColors: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeColors", required: false }] }], seatSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatSize", required: false }] }], seatGap: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatGap", required: false }] }], freeformBasis: [{ type: i0.Input, args: [{ isSignal: true, alias: "freeformBasis", required: false }] }], unitScale: [{ type: i0.Input, args: [{ isSignal: true, alias: "unitScale", required: false }] }], showSectionLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSectionLabels", required: false }] }], showLegend: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLegend", required: false }] }], showLegendPrices: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLegendPrices", required: false }] }], sectionIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "sectionIds", required: false }] }], sectionOverrides: [{ type: i0.Input, args: [{ isSignal: true, alias: "sectionOverrides", required: false }] }], selectableStatuses: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectableStatuses", required: false }] }], companionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "companionMode", required: false }] }], maxSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSelection", required: false }] }], seatSelectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatSelectable", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], expireHolds: [{ type: i0.Input, args: [{ isSignal: true, alias: "expireHolds", required: false }] }], expiryIntervalMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "expiryIntervalMs", required: false }] }], zoomable: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomable", required: false }] }], maxZoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxZoom", required: false }] }], showZoomControls: [{ type: i0.Input, args: [{ isSignal: true, alias: "showZoomControls", required: false }] }], maximizable: [{ type: i0.Input, args: [{ isSignal: true, alias: "maximizable", required: false }] }], controlStrings: [{ type: i0.Input, args: [{ isSignal: true, alias: "controlStrings", required: false }] }], validate: [{ type: i0.Input, args: [{ isSignal: true, alias: "validate", required: false }] }], seatSelect: [{ type: i0.Output, args: ["seatSelect"] }], seatDeselect: [{ type: i0.Output, args: ["seatDeselect"] }], seatDisallowed: [{ type: i0.Output, args: ["seatDisallowed"] }], seatFocus: [{ type: i0.Output, args: ["seatFocus"] }], validationIssues: [{ type: i0.Output, args: ["validationIssues"] }], viewportChange: [{ type: i0.Output, args: ["viewportChange"] }], maximizedChange: [{ type: i0.Output, args: ["maximizedChange"] }], viewportEl: [{ type: i0.ViewChild, args: ['viewport', { isSignal: true }] }], contentEl: [{ type: i0.ViewChild, args: ['content', { isSignal: true }] }] } });
|
|
767
1151
|
function formatTotal(total, locale) {
|
|
768
1152
|
return total ? formatMoney(total, locale) : '';
|
|
769
1153
|
}
|
|
1154
|
+
/** `:focus-visible` where the engine knows it; otherwise assume the focus is the keyboard's. */
|
|
1155
|
+
function focusIsVisible(node) {
|
|
1156
|
+
try {
|
|
1157
|
+
return node.matches(':focus-visible');
|
|
1158
|
+
}
|
|
1159
|
+
catch {
|
|
1160
|
+
return true;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
770
1163
|
|
|
771
1164
|
/**
|
|
772
1165
|
* The Kerusi-native seat map: `<kerusi-seatmap>`, its section renderer, and an
|