@kerusiweb/angular 2.1.0 → 2.3.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.
- package/README.md +311 -4
- package/fesm2022/kerusiweb-angular.mjs +500 -138
- package/fesm2022/kerusiweb-angular.mjs.map +1 -1
- package/package.json +2 -2
- package/types/kerusiweb-angular.d.ts +177 -60
|
@@ -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,
|
|
3
|
+
import { input, output, computed, ChangeDetectionStrategy, Component, inject, TemplateRef, Directive, ElementRef, model, contentChild, signal, viewChild, effect, untracked } from '@angular/core';
|
|
4
|
+
import { DEFAULT_KERUSI_COLORS, resolveSeatStyle, resolveSeatGlyph, cssVar, elementStyle, elementFill, elementTextFill, screenPath, legendSwatchFill, legendSwatchPath, legendSwatchText, seatBodyPath, seatSelectedFrame, seatOccupantPath, seatOccupantStroke, seatWashFill, occupantFillFor, occupantStrokeFor, seatCoreFill, 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>`.
|
|
@@ -32,6 +33,15 @@ class KerusiSectionComponent {
|
|
|
32
33
|
/** Whether an available seat may take its `SeatType.color` (§4.7). */
|
|
33
34
|
typeColors = input(true, /* @ts-ignore */
|
|
34
35
|
...(ngDevMode ? [{ debugName: "typeColors" }] : /* istanbul ignore next */ []));
|
|
36
|
+
/** Styles for every seat of a type, keyed by `SeatType.id`. */
|
|
37
|
+
seatTypeStyles = input(undefined, /* @ts-ignore */
|
|
38
|
+
...(ngDevMode ? [{ debugName: "seatTypeStyles" }] : /* istanbul ignore next */ []));
|
|
39
|
+
/** A style for one seat, laid over its type's. */
|
|
40
|
+
seatStyle = input(undefined, /* @ts-ignore */
|
|
41
|
+
...(ngDevMode ? [{ debugName: "seatStyle" }] : /* istanbul ignore next */ []));
|
|
42
|
+
/** Draws the inside of each seat in place of the stock glyph. */
|
|
43
|
+
seatTemplate = input(undefined, /* @ts-ignore */
|
|
44
|
+
...(ngDevMode ? [{ debugName: "seatTemplate" }] : /* istanbul ignore next */ []));
|
|
35
45
|
/** When false, seats render but do not respond to pointer or keyboard. */
|
|
36
46
|
interactive = input(true, /* @ts-ignore */
|
|
37
47
|
...(ngDevMode ? [{ debugName: "interactive" }] : /* istanbul ignore next */ []));
|
|
@@ -54,114 +64,29 @@ class KerusiSectionComponent {
|
|
|
54
64
|
/** The section's natural width in CSS pixels; it never draws wider. */
|
|
55
65
|
naturalWidth = computed(() => this.layout().width * this.unitScale(), /* @ts-ignore */
|
|
56
66
|
...(ngDevMode ? [{ debugName: "naturalWidth" }] : /* istanbul ignore next */ []));
|
|
57
|
-
isSelected(placed) {
|
|
58
|
-
return this.selection().has(placed.seat.id);
|
|
59
|
-
}
|
|
60
|
-
fill(placed) {
|
|
61
|
-
return seatFill(placed.seat, this.isSelected(placed), this.colors(), this.typeColors());
|
|
62
|
-
}
|
|
63
|
-
textFill(placed) {
|
|
64
|
-
return seatTextFill(placed.seat, this.isSelected(placed), this.colors(), this.typeColors());
|
|
65
|
-
}
|
|
66
|
-
backdrop() {
|
|
67
|
-
return cssVar('backdrop', this.colors().backdrop);
|
|
68
|
-
}
|
|
69
|
-
// --- the seat glyph -------------------------------------------------------
|
|
70
|
-
/** The seat's outline: square-ish front, tapered back — shape carries facing. */
|
|
71
|
-
bodyPath(placed) {
|
|
72
|
-
return seatBodyPath(placed.x, placed.y, placed.width, placed.height);
|
|
73
|
-
}
|
|
74
|
-
/** `null` for an available or blocked seat — nothing to mark. */
|
|
75
|
-
occupantVariant(placed) {
|
|
76
|
-
return seatOccupantVariant(placed.seat, this.isSelected(placed));
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* The box a seat's decorative marks are drawn to: the seat's own, or — when
|
|
80
|
-
* it is selected — the core plate's, which is inset by the frame width.
|
|
81
|
-
*
|
|
82
|
-
* Everything over a selected seat sits on the core, so it all has to be
|
|
83
|
-
* measured from the core. Drawing to the seat's box instead leaves the
|
|
84
|
-
* occupant's hips crossing the frame, where a figure tinted `selectedBg`
|
|
85
|
-
* lands on a frame filled `selectedBg` and reads as a hard nub. Since the
|
|
86
|
-
* core is an exact scaled copy of the body (see {@link seatSelectedFrame}),
|
|
87
|
-
* routing through here reproduces every clearance the mark had against the
|
|
88
|
-
* body, scaled — so the frame width stays free to change.
|
|
89
|
-
*/
|
|
90
|
-
markBox(placed) {
|
|
91
|
-
if (!this.isSelected(placed)) {
|
|
92
|
-
return placed;
|
|
93
|
-
}
|
|
94
|
-
const frame = seatSelectedFrame(placed.width, placed.height);
|
|
95
|
-
return {
|
|
96
|
-
x: placed.x + frame,
|
|
97
|
-
y: placed.y + frame,
|
|
98
|
-
width: placed.width - frame * 2,
|
|
99
|
-
height: placed.height - frame * 2,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
/** The bright plate inside a selected seat's frame. */
|
|
103
|
-
corePath(placed) {
|
|
104
|
-
const box = this.markBox(placed);
|
|
105
|
-
return seatBodyPath(box.x, box.y, box.width, box.height);
|
|
106
|
-
}
|
|
107
|
-
coreFill() {
|
|
108
|
-
return seatCoreFill(this.colors());
|
|
109
|
-
}
|
|
110
|
-
/** The dimming wash over a held or booked seat's type colour. */
|
|
111
|
-
washFill(placed) {
|
|
112
|
-
// Only called for held/booked seats — see the template's @if.
|
|
113
|
-
return seatWashFill(placed.seat.status, this.colors());
|
|
114
|
-
}
|
|
115
|
-
/** The occupant figure for a marked seat, sized to whatever it sits on. */
|
|
116
|
-
occupantPath(placed) {
|
|
117
|
-
const box = this.markBox(placed);
|
|
118
|
-
return seatOccupantPath(box.x, box.y, box.width, box.height);
|
|
119
|
-
}
|
|
120
|
-
occupantStroke(placed) {
|
|
121
|
-
const box = this.markBox(placed);
|
|
122
|
-
return seatOccupantStroke(box.width, box.height);
|
|
123
|
-
}
|
|
124
|
-
// The wheelchair marker keeps the same relative corner it always had; only
|
|
125
|
-
// the box it is relative to changes, so a selected seat's dot stays whole
|
|
126
|
-
// instead of being bitten off by the frame.
|
|
127
|
-
wheelchairCx(placed) {
|
|
128
|
-
const box = this.markBox(placed);
|
|
129
|
-
return box.x + box.width * 0.82;
|
|
130
|
-
}
|
|
131
|
-
wheelchairCy(placed) {
|
|
132
|
-
const box = this.markBox(placed);
|
|
133
|
-
return box.y + box.height * 0.18;
|
|
134
|
-
}
|
|
135
|
-
wheelchairRadius(placed) {
|
|
136
|
-
return this.markBox(placed).width * 0.13;
|
|
137
|
-
}
|
|
138
67
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* The label is centred on the occupant's shoulder line, so without this it is
|
|
143
|
-
* `selectedBg` over a `selectedBg` silhouette — the figure, not the plate, is
|
|
144
|
-
* what it actually has to contrast against. The halo also rescues a label too
|
|
145
|
-
* long for the core, which would otherwise run onto the frame and vanish into
|
|
146
|
-
* it.
|
|
147
|
-
*
|
|
148
|
-
* Bound here rather than in the stylesheet on purpose: a CSS
|
|
149
|
-
* `var(--kerusi-selected-fg, …)` rule would carry the library default as its
|
|
150
|
-
* fallback and silently skip the `[colors]` input tier, which only the
|
|
151
|
-
* TypeScript side can resolve.
|
|
68
|
+
* Every seat with its glyph resolved, once per change of layout, selection or
|
|
69
|
+
* theme rather than once per binding per change detection.
|
|
152
70
|
*/
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
71
|
+
seats = computed(() => {
|
|
72
|
+
const selection = this.selection();
|
|
73
|
+
const colors = this.colors();
|
|
74
|
+
const typeColors = this.typeColors();
|
|
75
|
+
const typeStyles = this.seatTypeStyles();
|
|
76
|
+
const seatStyle = this.seatStyle();
|
|
77
|
+
return this.layout().seats.map((placed) => {
|
|
78
|
+
const selected = selection.has(placed.seat.id);
|
|
79
|
+
const style = resolveSeatStyle(placed.seat, selected, typeStyles, seatStyle);
|
|
80
|
+
return {
|
|
81
|
+
placed,
|
|
82
|
+
selected,
|
|
83
|
+
glyph: resolveSeatGlyph(placed, selected, colors, typeColors, style),
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
}, /* @ts-ignore */
|
|
87
|
+
...(ngDevMode ? [{ debugName: "seats" }] : /* istanbul ignore next */ []));
|
|
88
|
+
backdrop() {
|
|
89
|
+
return cssVar('backdrop', this.colors().backdrop);
|
|
165
90
|
}
|
|
166
91
|
/** The section's single tab stop; every other seat is reachable by arrow key. */
|
|
167
92
|
tabIndex(placed) {
|
|
@@ -173,19 +98,6 @@ class KerusiSectionComponent {
|
|
|
173
98
|
ariaLabel(placed) {
|
|
174
99
|
return this.ariaLabels().get(placed.seat.id) ?? placed.seat.label;
|
|
175
100
|
}
|
|
176
|
-
seatClasses(placed) {
|
|
177
|
-
const seat = placed.seat;
|
|
178
|
-
return [
|
|
179
|
-
'kerusi-seat',
|
|
180
|
-
`kerusi-seat--${seat.status}`,
|
|
181
|
-
this.isSelected(placed) ? 'kerusi-seat--selected' : '',
|
|
182
|
-
seat.selectable ? '' : 'kerusi-seat--unselectable',
|
|
183
|
-
seat.accessibility?.wheelchairAccessible ? 'kerusi-seat--wheelchair' : '',
|
|
184
|
-
seat.companions.length > 0 ? 'kerusi-seat--companion' : '',
|
|
185
|
-
]
|
|
186
|
-
.filter(Boolean)
|
|
187
|
-
.join(' ');
|
|
188
|
-
}
|
|
189
101
|
/** SVG rotate() about the item's center; null when it does not rotate. */
|
|
190
102
|
transform(item) {
|
|
191
103
|
return item.rotation ? `rotate(${item.rotation} ${item.centerX} ${item.centerY})` : null;
|
|
@@ -237,12 +149,12 @@ class KerusiSectionComponent {
|
|
|
237
149
|
}
|
|
238
150
|
}
|
|
239
151
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: KerusiSectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
240
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: KerusiSectionComponent, isStandalone: true, selector: "kerusi-section", inputs: { section: { classPropertyName: "section", publicName: "section", isSignal: true, isRequired: true, transformFunction: null }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: true, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, focusedSeatId: { classPropertyName: "focusedSeatId", publicName: "focusedSeatId", isSignal: true, isRequired: false, transformFunction: null }, ariaLabels: { classPropertyName: "ariaLabels", publicName: "ariaLabels", 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 }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, unitScale: { classPropertyName: "unitScale", publicName: "unitScale", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { seatActivate: "seatActivate", seatFocused: "seatFocused", seatKeydown: "seatKeydown" }, ngImport: i0, template: "<svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n class=\"kerusi-section-svg\"\r\n role=\"presentation\"\r\n focusable=\"false\"\r\n [style.--kerusi-focus-ring-input]=\"colors().focusRing\"\r\n [attr.viewBox]=\"viewBox()\"\r\n [style.max-width.px]=\"naturalWidth()\"\r\n preserveAspectRatio=\"xMidYMid meet\"\r\n>\r\n <rect\r\n x=\"0\"\r\n y=\"0\"\r\n [attr.width]=\"layout().width\"\r\n [attr.height]=\"layout().height\"\r\n [style.fill]=\"backdrop()\"\r\n />\r\n\r\n <!-- Elements first, so a screen or stage sits behind the seats it faces. -->\r\n @for (placed of layout().elements; track placed.element.id) {\r\n @if (drawsElement(placed)) {\r\n <g\r\n class=\"kerusi-element kerusi-element--{{ placed.element.kind }}\"\r\n [attr.transform]=\"transform(placed)\"\r\n >\r\n @switch (style(placed).shape) {\r\n @case ('screen') {\r\n <path [attr.d]=\"screenPathOf(placed)\" [style.fill]=\"elementFillOf(placed)\" />\r\n }\r\n @case ('text') {}\r\n @case ('void') {\r\n <rect\r\n class=\"kerusi-element__void\"\r\n [attr.x]=\"placed.x\"\r\n [attr.y]=\"placed.y\"\r\n [attr.width]=\"placed.width\"\r\n [attr.height]=\"placed.height\"\r\n [style.stroke]=\"elementTextFillOf(placed)\"\r\n fill=\"none\"\r\n />\r\n }\r\n @default {\r\n <rect\r\n [attr.x]=\"placed.x\"\r\n [attr.y]=\"placed.y\"\r\n [attr.width]=\"placed.width\"\r\n [attr.height]=\"placed.height\"\r\n [attr.rx]=\"radiusOf(placed)\"\r\n [style.fill]=\"elementFillOf(placed)\"\r\n />\r\n }\r\n }\r\n @if (placed.element.label && style(placed).showLabel) {\r\n <text\r\n class=\"kerusi-element__label\"\r\n text-anchor=\"middle\"\r\n dominant-baseline=\"central\"\r\n [attr.x]=\"placed.centerX\"\r\n [attr.y]=\"placed.centerY\"\r\n [attr.font-size]=\"placed.fontSize\"\r\n [style.fill]=\"elementTextFillOf(placed)\"\r\n [attr.transform]=\"labelTransform(placed)\"\r\n >\r\n {{ placed.element.label }}\r\n </text>\r\n }\r\n </g>\r\n }\r\n }\r\n\r\n @for (placed of layout().seats; track placed.seat.id) {\r\n <g\r\n [class]=\"seatClasses(placed)\"\r\n role=\"button\"\r\n [attr.tabindex]=\"tabIndex(placed)\"\r\n [attr.aria-label]=\"ariaLabel(placed)\"\r\n [attr.aria-pressed]=\"isSelected(placed)\"\r\n [attr.aria-disabled]=\"!placed.seat.selectable\"\r\n [attr.data-seat-id]=\"placed.seat.id\"\r\n [attr.transform]=\"transform(placed)\"\r\n (click)=\"onSeatClick(placed)\"\r\n (focus)=\"onSeatFocus(placed)\"\r\n (keydown)=\"onKeydown($event)\"\r\n >\r\n @let variant = occupantVariant(placed);\r\n <!-- Tapered toward the back, square at the front, so which way the seat\r\n faces is legible from the outline alone. -->\r\n <path class=\"kerusi-seat__box\" [attr.d]=\"bodyPath(placed)\" [style.fill]=\"fill(placed)\" />\r\n @if (variant === 'held' || variant === 'booked') {\r\n <!-- Colour stays the seat type's; a taken seat recedes under a wash\r\n instead, so the type colour still reads on a busy map. -->\r\n <path\r\n class=\"kerusi-seat__wash\"\r\n [attr.d]=\"bodyPath(placed)\"\r\n [style.fill]=\"washFill(placed)\"\r\n />\r\n }\r\n @if (variant === 'selected') {\r\n <!-- Selection is the one state drawn from two tones: the body above is\r\n the frame, this is the bright core inside it. Whichever way the\r\n host page is themed, one of the two separates from it \u2014 which is\r\n why nothing here has to know about the page. -->\r\n <path class=\"kerusi-seat__core\" [attr.d]=\"corePath(placed)\" [style.fill]=\"coreFill()\" />\r\n }\r\n @if (variant) {\r\n <!-- Deliberately not counter-rotated: the occupant leans with the\r\n seat. Solid = settled (yours, or sold); hollow = a hold still in\r\n progress. -->\r\n <path\r\n class=\"kerusi-seat__occupant kerusi-seat__occupant--{{ variant }}\"\r\n [attr.d]=\"occupantPath(placed)\"\r\n [attr.stroke-width]=\"occupantStroke(placed)\"\r\n [style.fill]=\"occupantFill(variant)\"\r\n [style.stroke]=\"occupantStrokeColor(variant)\"\r\n />\r\n }\r\n <text\r\n class=\"kerusi-seat__label\"\r\n text-anchor=\"middle\"\r\n dominant-baseline=\"central\"\r\n [attr.x]=\"placed.centerX\"\r\n [attr.y]=\"placed.centerY\"\r\n [attr.font-size]=\"placed.fontSize\"\r\n [style.fill]=\"textFill(placed)\"\r\n [style.stroke]=\"labelHalo(placed)\"\r\n [attr.stroke-width]=\"labelHaloWidth(placed)\"\r\n [attr.transform]=\"counterRotate(placed)\"\r\n >\r\n {{ placed.seat.label }}\r\n </text>\r\n @if (placed.seat.accessibility?.wheelchairAccessible) {\r\n <!-- A corner marker, not a different fill: availability owns the fill. -->\r\n <circle\r\n class=\"kerusi-seat__wheelchair\"\r\n [attr.cx]=\"wheelchairCx(placed)\"\r\n [attr.cy]=\"wheelchairCy(placed)\"\r\n [attr.r]=\"wheelchairRadius(placed)\"\r\n [style.fill]=\"textFill(placed)\"\r\n />\r\n }\r\n </g>\r\n }\r\n</svg>\r\n", styles: [":host{display:block}.kerusi-section-svg{display:block;width:100%;height:auto;max-width:100%}.kerusi-seat{cursor:pointer;outline:none}.kerusi-seat--unselectable{cursor:not-allowed}.kerusi-seat__label,.kerusi-element__label{pointer-events:none;-webkit-user-select:none;user-select:none;font-family:inherit}.kerusi-seat__label{paint-order:stroke;stroke-linejoin:round}.kerusi-seat__wash{pointer-events:none;opacity:.5}.kerusi-seat__core,.kerusi-seat__occupant{pointer-events:none}.kerusi-seat__occupant--selected{opacity:.2}.kerusi-seat__occupant--booked{opacity:.55}.kerusi-seat__occupant--held{opacity:.75;stroke-linecap:round;stroke-linejoin:round}.kerusi-seat--selected .kerusi-seat__label{font-weight:600}.kerusi-seat:focus-visible .kerusi-seat__box{stroke:var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));stroke-width:2.5;paint-order:stroke}.kerusi-element__void{stroke-width:1;stroke-dasharray:4 3;opacity:.7}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
152
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: KerusiSectionComponent, isStandalone: true, selector: "kerusi-section", inputs: { section: { classPropertyName: "section", publicName: "section", isSignal: true, isRequired: true, transformFunction: null }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: true, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, focusedSeatId: { classPropertyName: "focusedSeatId", publicName: "focusedSeatId", isSignal: true, isRequired: false, transformFunction: null }, ariaLabels: { classPropertyName: "ariaLabels", publicName: "ariaLabels", 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 }, seatTypeStyles: { classPropertyName: "seatTypeStyles", publicName: "seatTypeStyles", isSignal: true, isRequired: false, transformFunction: null }, seatStyle: { classPropertyName: "seatStyle", publicName: "seatStyle", isSignal: true, isRequired: false, transformFunction: null }, seatTemplate: { classPropertyName: "seatTemplate", publicName: "seatTemplate", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, unitScale: { classPropertyName: "unitScale", publicName: "unitScale", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { seatActivate: "seatActivate", seatFocused: "seatFocused", seatKeydown: "seatKeydown" }, ngImport: i0, template: "<svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n class=\"kerusi-section-svg\"\r\n role=\"presentation\"\r\n focusable=\"false\"\r\n [style.--kerusi-focus-ring-input]=\"colors().focusRing\"\r\n [attr.viewBox]=\"viewBox()\"\r\n [style.max-width.px]=\"naturalWidth()\"\r\n preserveAspectRatio=\"xMidYMid meet\"\r\n>\r\n <rect\r\n x=\"0\"\r\n y=\"0\"\r\n [attr.width]=\"layout().width\"\r\n [attr.height]=\"layout().height\"\r\n [style.fill]=\"backdrop()\"\r\n />\r\n\r\n <!-- Elements first, so a screen or stage sits behind the seats it faces. -->\r\n @for (placed of layout().elements; track placed.element.id) {\r\n @if (drawsElement(placed)) {\r\n <g\r\n class=\"kerusi-element kerusi-element--{{ placed.element.kind }}\"\r\n [attr.transform]=\"transform(placed)\"\r\n >\r\n @switch (style(placed).shape) {\r\n @case ('screen') {\r\n <path [attr.d]=\"screenPathOf(placed)\" [style.fill]=\"elementFillOf(placed)\" />\r\n }\r\n @case ('text') {}\r\n @case ('void') {\r\n <rect\r\n class=\"kerusi-element__void\"\r\n [attr.x]=\"placed.x\"\r\n [attr.y]=\"placed.y\"\r\n [attr.width]=\"placed.width\"\r\n [attr.height]=\"placed.height\"\r\n [style.stroke]=\"elementTextFillOf(placed)\"\r\n fill=\"none\"\r\n />\r\n }\r\n @default {\r\n <rect\r\n [attr.x]=\"placed.x\"\r\n [attr.y]=\"placed.y\"\r\n [attr.width]=\"placed.width\"\r\n [attr.height]=\"placed.height\"\r\n [attr.rx]=\"radiusOf(placed)\"\r\n [style.fill]=\"elementFillOf(placed)\"\r\n />\r\n }\r\n }\r\n @if (placed.element.label && style(placed).showLabel) {\r\n <text\r\n class=\"kerusi-element__label\"\r\n text-anchor=\"middle\"\r\n dominant-baseline=\"central\"\r\n [attr.x]=\"placed.centerX\"\r\n [attr.y]=\"placed.centerY\"\r\n [attr.font-size]=\"placed.fontSize\"\r\n [style.fill]=\"elementTextFillOf(placed)\"\r\n [attr.transform]=\"labelTransform(placed)\"\r\n >\r\n {{ placed.element.label }}\r\n </text>\r\n }\r\n </g>\r\n }\r\n }\r\n\r\n <!-- The stock seat drawing. A template rather than inline markup so that a\r\n custom seat template can render it too, as `defaultContent`. Declared\r\n inside the <svg>, so its elements are created in the SVG namespace. -->\r\n <ng-template #stockSeat let-seat>\r\n @let glyph = seat.glyph;\r\n <!-- Tapered toward the back, square at the front, so which way the seat\r\n faces is legible from the outline alone. -->\r\n <path class=\"kerusi-seat__box\" [attr.d]=\"glyph.body.d\" [style.fill]=\"glyph.body.fill\" />\r\n @if (glyph.wash; as wash) {\r\n <!-- Colour stays the seat type's; a taken seat recedes under a wash\r\n instead, so the type colour still reads on a busy map. -->\r\n <path class=\"kerusi-seat__wash\" [attr.d]=\"wash.d\" [style.fill]=\"wash.fill\" />\r\n }\r\n @if (glyph.core; as core) {\r\n <!-- Selection is the one state drawn from two tones: the body above is\r\n the frame, this is the bright core inside it. Whichever way the\r\n host page is themed, one of the two separates from it \u2014 which is\r\n why nothing here has to know about the page. -->\r\n <path class=\"kerusi-seat__core\" [attr.d]=\"core.d\" [style.fill]=\"core.fill\" />\r\n }\r\n @if (glyph.occupant; as occupant) {\r\n <!-- Deliberately not counter-rotated: the occupant leans with the\r\n seat. Solid = settled (yours, or sold); hollow = a hold still in\r\n progress. -->\r\n <path\r\n class=\"kerusi-seat__occupant kerusi-seat__occupant--{{ occupant.variant }}\"\r\n [attr.d]=\"occupant.d\"\r\n [attr.stroke-width]=\"occupant.strokeWidth\"\r\n [style.fill]=\"occupant.fill\"\r\n [style.stroke]=\"occupant.stroke\"\r\n />\r\n }\r\n <!-- The halo colour and width are bound here rather than in the\r\n stylesheet: a CSS `var(--kerusi-selected-fg, \u2026)` rule would carry\r\n the library default as its fallback and skip the [colors] tier. -->\r\n <text\r\n class=\"kerusi-seat__label\"\r\n text-anchor=\"middle\"\r\n dominant-baseline=\"central\"\r\n [attr.x]=\"glyph.label.x\"\r\n [attr.y]=\"glyph.label.y\"\r\n [attr.font-size]=\"glyph.label.fontSize\"\r\n [style.fill]=\"glyph.label.fill\"\r\n [style.stroke]=\"glyph.label.halo\"\r\n [attr.stroke-width]=\"glyph.label.haloWidth\"\r\n [attr.transform]=\"glyph.label.transform\"\r\n >\r\n {{ glyph.label.text }}\r\n </text>\r\n @if (glyph.wheelchair; as wheelchair) {\r\n <!-- A corner marker, not a different fill: availability owns the fill. -->\r\n <circle\r\n class=\"kerusi-seat__wheelchair\"\r\n [attr.cx]=\"wheelchair.cx\"\r\n [attr.cy]=\"wheelchair.cy\"\r\n [attr.r]=\"wheelchair.r\"\r\n [style.fill]=\"wheelchair.fill\"\r\n />\r\n }\r\n </ng-template>\r\n\r\n @for (entry of seats(); track entry.placed.seat.id) {\r\n @let placed = entry.placed;\r\n @let glyph = entry.glyph;\r\n @let ctx =\r\n {\r\n placed: placed,\r\n seat: placed.seat,\r\n selected: entry.selected,\r\n glyph: glyph,\r\n defaultContent: stockSeat,\r\n };\r\n <g\r\n [class]=\"glyph.className\"\r\n role=\"button\"\r\n [attr.tabindex]=\"tabIndex(placed)\"\r\n [attr.aria-label]=\"ariaLabel(placed)\"\r\n [attr.aria-pressed]=\"entry.selected\"\r\n [attr.aria-disabled]=\"!placed.seat.selectable\"\r\n [attr.data-seat-id]=\"placed.seat.id\"\r\n [attr.data-seat-type]=\"glyph.data.type\"\r\n [attr.data-seat-status]=\"glyph.data.status\"\r\n [attr.data-section-id]=\"glyph.data.section\"\r\n [attr.data-seat-row]=\"glyph.data.row\"\r\n [attr.data-seat-attributes]=\"glyph.data.attributes\"\r\n [attr.transform]=\"transform(placed)\"\r\n (click)=\"onSeatClick(placed)\"\r\n (focus)=\"onSeatFocus(placed)\"\r\n (keydown)=\"onKeydown($event)\"\r\n >\r\n @if (seatTemplate(); as custom) {\r\n <ng-container *ngTemplateOutlet=\"custom; context: { $implicit: ctx }\" />\r\n <!-- Drawn over the custom content, because that content may have no\r\n .kerusi-seat__box for the stock focus ring to stroke. Only visible\r\n on :focus-visible. -->\r\n <path class=\"kerusi-seat__focus\" [attr.d]=\"glyph.body.d\" />\r\n } @else {\r\n <ng-container *ngTemplateOutlet=\"stockSeat; context: { $implicit: ctx }\" />\r\n }\r\n </g>\r\n }\r\n</svg>\r\n", styles: [":host{display:block}.kerusi-section-svg{display:block;width:100%;height:auto;max-width:100%}.kerusi-seat{cursor:pointer;outline:none}.kerusi-seat--unselectable{cursor:not-allowed}.kerusi-seat__label,.kerusi-element__label{pointer-events:none;-webkit-user-select:none;user-select:none;font-family:inherit}.kerusi-seat__label{paint-order:stroke;stroke-linejoin:round}.kerusi-seat__wash{pointer-events:none;opacity:.5}.kerusi-seat__core,.kerusi-seat__occupant{pointer-events:none}.kerusi-seat__occupant--selected{opacity:.2}.kerusi-seat__occupant--booked{opacity:.55}.kerusi-seat__occupant--held{opacity:.75;stroke-linecap:round;stroke-linejoin:round}.kerusi-seat--selected .kerusi-seat__label{font-weight:600}.kerusi-seat:focus-visible .kerusi-seat__box{stroke:var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));stroke-width:2.5;paint-order:stroke}.kerusi-seat__focus{fill:none;stroke:none;pointer-events:none}.kerusi-seat:focus-visible .kerusi-seat__focus{stroke:var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));stroke-width:2.5}.kerusi-element__void{stroke-width:1;stroke-dasharray:4 3;opacity:.7}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
241
153
|
}
|
|
242
154
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: KerusiSectionComponent, decorators: [{
|
|
243
155
|
type: Component,
|
|
244
|
-
args: [{ selector: 'kerusi-section', changeDetection: ChangeDetectionStrategy.OnPush, template: "<svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n class=\"kerusi-section-svg\"\r\n role=\"presentation\"\r\n focusable=\"false\"\r\n [style.--kerusi-focus-ring-input]=\"colors().focusRing\"\r\n [attr.viewBox]=\"viewBox()\"\r\n [style.max-width.px]=\"naturalWidth()\"\r\n preserveAspectRatio=\"xMidYMid meet\"\r\n>\r\n <rect\r\n x=\"0\"\r\n y=\"0\"\r\n [attr.width]=\"layout().width\"\r\n [attr.height]=\"layout().height\"\r\n [style.fill]=\"backdrop()\"\r\n />\r\n\r\n <!-- Elements first, so a screen or stage sits behind the seats it faces. -->\r\n @for (placed of layout().elements; track placed.element.id) {\r\n @if (drawsElement(placed)) {\r\n <g\r\n class=\"kerusi-element kerusi-element--{{ placed.element.kind }}\"\r\n [attr.transform]=\"transform(placed)\"\r\n >\r\n @switch (style(placed).shape) {\r\n @case ('screen') {\r\n <path [attr.d]=\"screenPathOf(placed)\" [style.fill]=\"elementFillOf(placed)\" />\r\n }\r\n @case ('text') {}\r\n @case ('void') {\r\n <rect\r\n class=\"kerusi-element__void\"\r\n [attr.x]=\"placed.x\"\r\n [attr.y]=\"placed.y\"\r\n [attr.width]=\"placed.width\"\r\n [attr.height]=\"placed.height\"\r\n [style.stroke]=\"elementTextFillOf(placed)\"\r\n fill=\"none\"\r\n />\r\n }\r\n @default {\r\n <rect\r\n [attr.x]=\"placed.x\"\r\n [attr.y]=\"placed.y\"\r\n [attr.width]=\"placed.width\"\r\n [attr.height]=\"placed.height\"\r\n [attr.rx]=\"radiusOf(placed)\"\r\n [style.fill]=\"elementFillOf(placed)\"\r\n />\r\n }\r\n }\r\n @if (placed.element.label && style(placed).showLabel) {\r\n <text\r\n class=\"kerusi-element__label\"\r\n text-anchor=\"middle\"\r\n dominant-baseline=\"central\"\r\n [attr.x]=\"placed.centerX\"\r\n [attr.y]=\"placed.centerY\"\r\n [attr.font-size]=\"placed.fontSize\"\r\n [style.fill]=\"elementTextFillOf(placed)\"\r\n [attr.transform]=\"labelTransform(placed)\"\r\n >\r\n {{ placed.element.label }}\r\n </text>\r\n }\r\n </g>\r\n }\r\n }\r\n\r\n
|
|
245
|
-
}], propDecorators: { section: [{ type: i0.Input, args: [{ isSignal: true, alias: "section", required: true }] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: true }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }], focusedSeatId: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusedSeatId", required: false }] }], ariaLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabels", required: false }] }], colors: [{ type: i0.Input, args: [{ isSignal: true, alias: "colors", required: false }] }], typeColors: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeColors", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], unitScale: [{ type: i0.Input, args: [{ isSignal: true, alias: "unitScale", required: false }] }], seatActivate: [{ type: i0.Output, args: ["seatActivate"] }], seatFocused: [{ type: i0.Output, args: ["seatFocused"] }], seatKeydown: [{ type: i0.Output, args: ["seatKeydown"] }] } });
|
|
156
|
+
args: [{ selector: 'kerusi-section', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgTemplateOutlet], template: "<svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n class=\"kerusi-section-svg\"\r\n role=\"presentation\"\r\n focusable=\"false\"\r\n [style.--kerusi-focus-ring-input]=\"colors().focusRing\"\r\n [attr.viewBox]=\"viewBox()\"\r\n [style.max-width.px]=\"naturalWidth()\"\r\n preserveAspectRatio=\"xMidYMid meet\"\r\n>\r\n <rect\r\n x=\"0\"\r\n y=\"0\"\r\n [attr.width]=\"layout().width\"\r\n [attr.height]=\"layout().height\"\r\n [style.fill]=\"backdrop()\"\r\n />\r\n\r\n <!-- Elements first, so a screen or stage sits behind the seats it faces. -->\r\n @for (placed of layout().elements; track placed.element.id) {\r\n @if (drawsElement(placed)) {\r\n <g\r\n class=\"kerusi-element kerusi-element--{{ placed.element.kind }}\"\r\n [attr.transform]=\"transform(placed)\"\r\n >\r\n @switch (style(placed).shape) {\r\n @case ('screen') {\r\n <path [attr.d]=\"screenPathOf(placed)\" [style.fill]=\"elementFillOf(placed)\" />\r\n }\r\n @case ('text') {}\r\n @case ('void') {\r\n <rect\r\n class=\"kerusi-element__void\"\r\n [attr.x]=\"placed.x\"\r\n [attr.y]=\"placed.y\"\r\n [attr.width]=\"placed.width\"\r\n [attr.height]=\"placed.height\"\r\n [style.stroke]=\"elementTextFillOf(placed)\"\r\n fill=\"none\"\r\n />\r\n }\r\n @default {\r\n <rect\r\n [attr.x]=\"placed.x\"\r\n [attr.y]=\"placed.y\"\r\n [attr.width]=\"placed.width\"\r\n [attr.height]=\"placed.height\"\r\n [attr.rx]=\"radiusOf(placed)\"\r\n [style.fill]=\"elementFillOf(placed)\"\r\n />\r\n }\r\n }\r\n @if (placed.element.label && style(placed).showLabel) {\r\n <text\r\n class=\"kerusi-element__label\"\r\n text-anchor=\"middle\"\r\n dominant-baseline=\"central\"\r\n [attr.x]=\"placed.centerX\"\r\n [attr.y]=\"placed.centerY\"\r\n [attr.font-size]=\"placed.fontSize\"\r\n [style.fill]=\"elementTextFillOf(placed)\"\r\n [attr.transform]=\"labelTransform(placed)\"\r\n >\r\n {{ placed.element.label }}\r\n </text>\r\n }\r\n </g>\r\n }\r\n }\r\n\r\n <!-- The stock seat drawing. A template rather than inline markup so that a\r\n custom seat template can render it too, as `defaultContent`. Declared\r\n inside the <svg>, so its elements are created in the SVG namespace. -->\r\n <ng-template #stockSeat let-seat>\r\n @let glyph = seat.glyph;\r\n <!-- Tapered toward the back, square at the front, so which way the seat\r\n faces is legible from the outline alone. -->\r\n <path class=\"kerusi-seat__box\" [attr.d]=\"glyph.body.d\" [style.fill]=\"glyph.body.fill\" />\r\n @if (glyph.wash; as wash) {\r\n <!-- Colour stays the seat type's; a taken seat recedes under a wash\r\n instead, so the type colour still reads on a busy map. -->\r\n <path class=\"kerusi-seat__wash\" [attr.d]=\"wash.d\" [style.fill]=\"wash.fill\" />\r\n }\r\n @if (glyph.core; as core) {\r\n <!-- Selection is the one state drawn from two tones: the body above is\r\n the frame, this is the bright core inside it. Whichever way the\r\n host page is themed, one of the two separates from it \u2014 which is\r\n why nothing here has to know about the page. -->\r\n <path class=\"kerusi-seat__core\" [attr.d]=\"core.d\" [style.fill]=\"core.fill\" />\r\n }\r\n @if (glyph.occupant; as occupant) {\r\n <!-- Deliberately not counter-rotated: the occupant leans with the\r\n seat. Solid = settled (yours, or sold); hollow = a hold still in\r\n progress. -->\r\n <path\r\n class=\"kerusi-seat__occupant kerusi-seat__occupant--{{ occupant.variant }}\"\r\n [attr.d]=\"occupant.d\"\r\n [attr.stroke-width]=\"occupant.strokeWidth\"\r\n [style.fill]=\"occupant.fill\"\r\n [style.stroke]=\"occupant.stroke\"\r\n />\r\n }\r\n <!-- The halo colour and width are bound here rather than in the\r\n stylesheet: a CSS `var(--kerusi-selected-fg, \u2026)` rule would carry\r\n the library default as its fallback and skip the [colors] tier. -->\r\n <text\r\n class=\"kerusi-seat__label\"\r\n text-anchor=\"middle\"\r\n dominant-baseline=\"central\"\r\n [attr.x]=\"glyph.label.x\"\r\n [attr.y]=\"glyph.label.y\"\r\n [attr.font-size]=\"glyph.label.fontSize\"\r\n [style.fill]=\"glyph.label.fill\"\r\n [style.stroke]=\"glyph.label.halo\"\r\n [attr.stroke-width]=\"glyph.label.haloWidth\"\r\n [attr.transform]=\"glyph.label.transform\"\r\n >\r\n {{ glyph.label.text }}\r\n </text>\r\n @if (glyph.wheelchair; as wheelchair) {\r\n <!-- A corner marker, not a different fill: availability owns the fill. -->\r\n <circle\r\n class=\"kerusi-seat__wheelchair\"\r\n [attr.cx]=\"wheelchair.cx\"\r\n [attr.cy]=\"wheelchair.cy\"\r\n [attr.r]=\"wheelchair.r\"\r\n [style.fill]=\"wheelchair.fill\"\r\n />\r\n }\r\n </ng-template>\r\n\r\n @for (entry of seats(); track entry.placed.seat.id) {\r\n @let placed = entry.placed;\r\n @let glyph = entry.glyph;\r\n @let ctx =\r\n {\r\n placed: placed,\r\n seat: placed.seat,\r\n selected: entry.selected,\r\n glyph: glyph,\r\n defaultContent: stockSeat,\r\n };\r\n <g\r\n [class]=\"glyph.className\"\r\n role=\"button\"\r\n [attr.tabindex]=\"tabIndex(placed)\"\r\n [attr.aria-label]=\"ariaLabel(placed)\"\r\n [attr.aria-pressed]=\"entry.selected\"\r\n [attr.aria-disabled]=\"!placed.seat.selectable\"\r\n [attr.data-seat-id]=\"placed.seat.id\"\r\n [attr.data-seat-type]=\"glyph.data.type\"\r\n [attr.data-seat-status]=\"glyph.data.status\"\r\n [attr.data-section-id]=\"glyph.data.section\"\r\n [attr.data-seat-row]=\"glyph.data.row\"\r\n [attr.data-seat-attributes]=\"glyph.data.attributes\"\r\n [attr.transform]=\"transform(placed)\"\r\n (click)=\"onSeatClick(placed)\"\r\n (focus)=\"onSeatFocus(placed)\"\r\n (keydown)=\"onKeydown($event)\"\r\n >\r\n @if (seatTemplate(); as custom) {\r\n <ng-container *ngTemplateOutlet=\"custom; context: { $implicit: ctx }\" />\r\n <!-- Drawn over the custom content, because that content may have no\r\n .kerusi-seat__box for the stock focus ring to stroke. Only visible\r\n on :focus-visible. -->\r\n <path class=\"kerusi-seat__focus\" [attr.d]=\"glyph.body.d\" />\r\n } @else {\r\n <ng-container *ngTemplateOutlet=\"stockSeat; context: { $implicit: ctx }\" />\r\n }\r\n </g>\r\n }\r\n</svg>\r\n", styles: [":host{display:block}.kerusi-section-svg{display:block;width:100%;height:auto;max-width:100%}.kerusi-seat{cursor:pointer;outline:none}.kerusi-seat--unselectable{cursor:not-allowed}.kerusi-seat__label,.kerusi-element__label{pointer-events:none;-webkit-user-select:none;user-select:none;font-family:inherit}.kerusi-seat__label{paint-order:stroke;stroke-linejoin:round}.kerusi-seat__wash{pointer-events:none;opacity:.5}.kerusi-seat__core,.kerusi-seat__occupant{pointer-events:none}.kerusi-seat__occupant--selected{opacity:.2}.kerusi-seat__occupant--booked{opacity:.55}.kerusi-seat__occupant--held{opacity:.75;stroke-linecap:round;stroke-linejoin:round}.kerusi-seat--selected .kerusi-seat__label{font-weight:600}.kerusi-seat:focus-visible .kerusi-seat__box{stroke:var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));stroke-width:2.5;paint-order:stroke}.kerusi-seat__focus{fill:none;stroke:none;pointer-events:none}.kerusi-seat:focus-visible .kerusi-seat__focus{stroke:var(--kerusi-focus-ring, var(--kerusi-focus-ring-input, currentColor));stroke-width:2.5}.kerusi-element__void{stroke-width:1;stroke-dasharray:4 3;opacity:.7}\n"] }]
|
|
157
|
+
}], propDecorators: { section: [{ type: i0.Input, args: [{ isSignal: true, alias: "section", required: true }] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: true }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }], focusedSeatId: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusedSeatId", required: false }] }], ariaLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabels", required: false }] }], colors: [{ type: i0.Input, args: [{ isSignal: true, alias: "colors", required: false }] }], typeColors: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeColors", required: false }] }], seatTypeStyles: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatTypeStyles", required: false }] }], seatStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatStyle", required: false }] }], seatTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatTemplate", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], unitScale: [{ type: i0.Input, args: [{ isSignal: true, alias: "unitScale", required: false }] }], seatActivate: [{ type: i0.Output, args: ["seatActivate"] }], seatFocused: [{ type: i0.Output, args: ["seatFocused"] }], seatKeydown: [{ type: i0.Output, args: ["seatKeydown"] }] } });
|
|
246
158
|
|
|
247
159
|
/**
|
|
248
160
|
* A key for a seat map: its seat types on one side, its availability states on
|
|
@@ -268,6 +180,9 @@ class KerusiLegendComponent {
|
|
|
268
180
|
/** Whether an available seat takes its type color — mirrors the seatmap input. */
|
|
269
181
|
typeColors = input(true, /* @ts-ignore */
|
|
270
182
|
...(ngDevMode ? [{ debugName: "typeColors" }] : /* istanbul ignore next */ []));
|
|
183
|
+
/** Mirrors the seatmap input: a type's style is what its swatch shows. */
|
|
184
|
+
seatTypeStyles = input(undefined, /* @ts-ignore */
|
|
185
|
+
...(ngDevMode ? [{ debugName: "seatTypeStyles" }] : /* istanbul ignore next */ []));
|
|
271
186
|
showPrices = input(true, /* @ts-ignore */
|
|
272
187
|
...(ngDevMode ? [{ debugName: "showPrices" }] : /* istanbul ignore next */ []));
|
|
273
188
|
/** Hide types no seat in the rendered map actually uses. */
|
|
@@ -301,13 +216,17 @@ class KerusiLegendComponent {
|
|
|
301
216
|
];
|
|
302
217
|
}, /* @ts-ignore */
|
|
303
218
|
...(ngDevMode ? [{ debugName: "statuses" }] : /* istanbul ignore next */ []));
|
|
219
|
+
// Same token and fallback as an available seat of the type, so a rule that
|
|
220
|
+
// sets `--kerusi-seat-fill` on the seats and the legend items keeps them in step.
|
|
304
221
|
swatch(entry) {
|
|
305
|
-
return (this.
|
|
222
|
+
return legendSwatchFill(entry, this.colors(), this.typeColors(), this.seatTypeStyles()?.[entry.id]);
|
|
223
|
+
}
|
|
224
|
+
/** The swatch outline for a type styled with its own shape; `undefined` keeps the square. */
|
|
225
|
+
typeShape(entry) {
|
|
226
|
+
return legendSwatchPath(this.seatTypeStyles()?.[entry.id], this.glyphSize);
|
|
306
227
|
}
|
|
307
228
|
swatchText(entry) {
|
|
308
|
-
|
|
309
|
-
// Mirrors `seatTextFill`: only a document-supplied color needs guessing at.
|
|
310
|
-
return typeColor ? readableOn(typeColor) : cssVar('availableFg', this.colors().availableFg);
|
|
229
|
+
return legendSwatchText(entry, this.colors(), this.typeColors(), this.seatTypeStyles()?.[entry.id]);
|
|
311
230
|
}
|
|
312
231
|
// --- status swatches -------------------------------------------------------
|
|
313
232
|
//
|
|
@@ -355,12 +274,44 @@ class KerusiLegendComponent {
|
|
|
355
274
|
return entry.price ? formatMoney(entry.price, this.locale()) : '';
|
|
356
275
|
}
|
|
357
276
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: KerusiLegendComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
358
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: KerusiLegendComponent, isStandalone: true, selector: "kerusi-legend", inputs: { legend: { classPropertyName: "legend", publicName: "legend", isSignal: true, isRequired: true, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", 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 }, showPrices: { classPropertyName: "showPrices", publicName: "showPrices", isSignal: true, isRequired: false, transformFunction: null }, hideUnusedTypes: { classPropertyName: "hideUnusedTypes", publicName: "hideUnusedTypes", isSignal: true, isRequired: false, transformFunction: null }, headingSeatTypes: { classPropertyName: "headingSeatTypes", publicName: "headingSeatTypes", isSignal: true, isRequired: false, transformFunction: null }, headingAvailability: { classPropertyName: "headingAvailability", publicName: "headingAvailability", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"kerusi-legend\">\r\n @if (types().length) {\r\n <div class=\"kerusi-legend__block\">\r\n <h4 class=\"kerusi-legend__heading\">{{ headingSeatTypes() }}</h4>\r\n <ul class=\"kerusi-legend__list\">\r\n @for (entry of types(); track entry.id) {\r\n <li class=\"kerusi-legend__item\">\r\n <
|
|
277
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: KerusiLegendComponent, isStandalone: true, selector: "kerusi-legend", inputs: { legend: { classPropertyName: "legend", publicName: "legend", isSignal: true, isRequired: true, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", 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 }, seatTypeStyles: { classPropertyName: "seatTypeStyles", publicName: "seatTypeStyles", isSignal: true, isRequired: false, transformFunction: null }, showPrices: { classPropertyName: "showPrices", publicName: "showPrices", isSignal: true, isRequired: false, transformFunction: null }, hideUnusedTypes: { classPropertyName: "hideUnusedTypes", publicName: "hideUnusedTypes", isSignal: true, isRequired: false, transformFunction: null }, headingSeatTypes: { classPropertyName: "headingSeatTypes", publicName: "headingSeatTypes", isSignal: true, isRequired: false, transformFunction: null }, headingAvailability: { classPropertyName: "headingAvailability", publicName: "headingAvailability", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"kerusi-legend\">\r\n @if (types().length) {\r\n <div class=\"kerusi-legend__block\">\r\n <h4 class=\"kerusi-legend__heading\">{{ headingSeatTypes() }}</h4>\r\n <ul class=\"kerusi-legend__list\">\r\n @for (entry of types(); track entry.id) {\r\n <li class=\"kerusi-legend__item\" [attr.data-seat-type]=\"entry.id\">\r\n @if (typeShape(entry); as d) {\r\n <!-- A type styled with its own shape keys that shape, not a square. -->\r\n <svg\r\n class=\"kerusi-legend__swatch kerusi-legend__swatch--glyph\"\r\n aria-hidden=\"true\"\r\n focusable=\"false\"\r\n [attr.viewBox]=\"'0 0 ' + glyphSize + ' ' + glyphSize\"\r\n >\r\n <path [attr.d]=\"d\" [style.fill]=\"swatch(entry)\" />\r\n </svg>\r\n } @else {\r\n <span\r\n class=\"kerusi-legend__swatch\"\r\n aria-hidden=\"true\"\r\n [style.background]=\"swatch(entry)\"\r\n [style.color]=\"swatchText(entry)\"\r\n ></span>\r\n }\r\n <span class=\"kerusi-legend__label\">{{ entry.label }}</span>\r\n @if (showPrices() && price(entry)) {\r\n <span class=\"kerusi-legend__price\">{{ price(entry) }}</span>\r\n }\r\n </li>\r\n }\r\n </ul>\r\n </div>\r\n }\r\n\r\n <div class=\"kerusi-legend__block\">\r\n <h4 class=\"kerusi-legend__heading\">{{ headingAvailability() }}</h4>\r\n <ul class=\"kerusi-legend__list\">\r\n @for (status of statuses(); track status.key) {\r\n <li class=\"kerusi-legend__item\">\r\n <!-- Shape carries status now, so every swatch draws the seat glyph\r\n rather than a flat colour \u2014 a flat swatch would teach a cue the\r\n map itself no longer uses. -->\r\n <svg\r\n class=\"kerusi-legend__swatch kerusi-legend__swatch--glyph\"\r\n aria-hidden=\"true\"\r\n focusable=\"false\"\r\n [attr.viewBox]=\"'0 0 ' + glyphSize + ' ' + glyphSize\"\r\n >\r\n <path [attr.d]=\"glyphBody\" [style.fill]=\"status.fill\" />\r\n @if (hasWash(status.kind)) {\r\n <path [attr.d]=\"glyphBody\" [style.fill]=\"washFillOf(status.kind)\" opacity=\"0.5\" />\r\n }\r\n @if (status.kind === 'selected') {\r\n <!-- Two tones, same as the map: the body above is the frame, this\r\n is the core inside it. -->\r\n <path [attr.d]=\"glyphCore\" [style.fill]=\"coreFill()\" />\r\n }\r\n @if (hasOccupant(status.kind)) {\r\n <path\r\n [attr.d]=\"occupantPathOf(status.kind)\"\r\n class=\"kerusi-legend__occupant kerusi-legend__occupant--{{ status.kind }}\"\r\n [attr.stroke-width]=\"occupantStrokeWidthOf(status.kind)\"\r\n [style.fill]=\"occupantFillOf(status.kind)\"\r\n [style.stroke]=\"occupantStrokeOf(status.kind)\"\r\n />\r\n }\r\n </svg>\r\n <span class=\"kerusi-legend__label\">{{ status.label }}</span>\r\n </li>\r\n }\r\n </ul>\r\n </div>\r\n</div>\r\n", styles: [":host{display:block}.kerusi-legend{display:flex;flex-wrap:wrap;gap:1.5rem;font-size:.85rem}.kerusi-legend__block{display:flex;flex-direction:column;gap:.4rem;min-width:0}.kerusi-legend__heading{margin:0;font-size:.72rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;opacity:.65}.kerusi-legend__list{display:flex;flex-wrap:wrap;gap:.35rem .9rem;margin:0;padding:0;list-style:none}.kerusi-legend__item{display:inline-flex;align-items:center;gap:.4rem}.kerusi-legend__swatch{display:inline-block;flex:none;width:.85rem;height:.85rem;border-radius:3px}.kerusi-legend__swatch--glyph{border-radius:0;overflow:visible}.kerusi-legend__occupant--selected{opacity:.2}.kerusi-legend__occupant--booked{opacity:.55}.kerusi-legend__occupant--held{opacity:.75;stroke-linecap:round;stroke-linejoin:round}.kerusi-legend__price{opacity:.7;font-variant-numeric:tabular-nums}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
359
278
|
}
|
|
360
279
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: KerusiLegendComponent, decorators: [{
|
|
361
280
|
type: Component,
|
|
362
|
-
args: [{ selector: 'kerusi-legend', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"kerusi-legend\">\r\n @if (types().length) {\r\n <div class=\"kerusi-legend__block\">\r\n <h4 class=\"kerusi-legend__heading\">{{ headingSeatTypes() }}</h4>\r\n <ul class=\"kerusi-legend__list\">\r\n @for (entry of types(); track entry.id) {\r\n <li class=\"kerusi-legend__item\">\r\n <
|
|
363
|
-
}], propDecorators: { legend: [{ type: i0.Input, args: [{ isSignal: true, alias: "legend", required: true }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], colors: [{ type: i0.Input, args: [{ isSignal: true, alias: "colors", required: false }] }], typeColors: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeColors", required: false }] }], showPrices: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPrices", required: false }] }], hideUnusedTypes: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideUnusedTypes", required: false }] }], headingSeatTypes: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingSeatTypes", required: false }] }], headingAvailability: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingAvailability", required: false }] }] } });
|
|
281
|
+
args: [{ selector: 'kerusi-legend', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"kerusi-legend\">\r\n @if (types().length) {\r\n <div class=\"kerusi-legend__block\">\r\n <h4 class=\"kerusi-legend__heading\">{{ headingSeatTypes() }}</h4>\r\n <ul class=\"kerusi-legend__list\">\r\n @for (entry of types(); track entry.id) {\r\n <li class=\"kerusi-legend__item\" [attr.data-seat-type]=\"entry.id\">\r\n @if (typeShape(entry); as d) {\r\n <!-- A type styled with its own shape keys that shape, not a square. -->\r\n <svg\r\n class=\"kerusi-legend__swatch kerusi-legend__swatch--glyph\"\r\n aria-hidden=\"true\"\r\n focusable=\"false\"\r\n [attr.viewBox]=\"'0 0 ' + glyphSize + ' ' + glyphSize\"\r\n >\r\n <path [attr.d]=\"d\" [style.fill]=\"swatch(entry)\" />\r\n </svg>\r\n } @else {\r\n <span\r\n class=\"kerusi-legend__swatch\"\r\n aria-hidden=\"true\"\r\n [style.background]=\"swatch(entry)\"\r\n [style.color]=\"swatchText(entry)\"\r\n ></span>\r\n }\r\n <span class=\"kerusi-legend__label\">{{ entry.label }}</span>\r\n @if (showPrices() && price(entry)) {\r\n <span class=\"kerusi-legend__price\">{{ price(entry) }}</span>\r\n }\r\n </li>\r\n }\r\n </ul>\r\n </div>\r\n }\r\n\r\n <div class=\"kerusi-legend__block\">\r\n <h4 class=\"kerusi-legend__heading\">{{ headingAvailability() }}</h4>\r\n <ul class=\"kerusi-legend__list\">\r\n @for (status of statuses(); track status.key) {\r\n <li class=\"kerusi-legend__item\">\r\n <!-- Shape carries status now, so every swatch draws the seat glyph\r\n rather than a flat colour \u2014 a flat swatch would teach a cue the\r\n map itself no longer uses. -->\r\n <svg\r\n class=\"kerusi-legend__swatch kerusi-legend__swatch--glyph\"\r\n aria-hidden=\"true\"\r\n focusable=\"false\"\r\n [attr.viewBox]=\"'0 0 ' + glyphSize + ' ' + glyphSize\"\r\n >\r\n <path [attr.d]=\"glyphBody\" [style.fill]=\"status.fill\" />\r\n @if (hasWash(status.kind)) {\r\n <path [attr.d]=\"glyphBody\" [style.fill]=\"washFillOf(status.kind)\" opacity=\"0.5\" />\r\n }\r\n @if (status.kind === 'selected') {\r\n <!-- Two tones, same as the map: the body above is the frame, this\r\n is the core inside it. -->\r\n <path [attr.d]=\"glyphCore\" [style.fill]=\"coreFill()\" />\r\n }\r\n @if (hasOccupant(status.kind)) {\r\n <path\r\n [attr.d]=\"occupantPathOf(status.kind)\"\r\n class=\"kerusi-legend__occupant kerusi-legend__occupant--{{ status.kind }}\"\r\n [attr.stroke-width]=\"occupantStrokeWidthOf(status.kind)\"\r\n [style.fill]=\"occupantFillOf(status.kind)\"\r\n [style.stroke]=\"occupantStrokeOf(status.kind)\"\r\n />\r\n }\r\n </svg>\r\n <span class=\"kerusi-legend__label\">{{ status.label }}</span>\r\n </li>\r\n }\r\n </ul>\r\n </div>\r\n</div>\r\n", styles: [":host{display:block}.kerusi-legend{display:flex;flex-wrap:wrap;gap:1.5rem;font-size:.85rem}.kerusi-legend__block{display:flex;flex-direction:column;gap:.4rem;min-width:0}.kerusi-legend__heading{margin:0;font-size:.72rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;opacity:.65}.kerusi-legend__list{display:flex;flex-wrap:wrap;gap:.35rem .9rem;margin:0;padding:0;list-style:none}.kerusi-legend__item{display:inline-flex;align-items:center;gap:.4rem}.kerusi-legend__swatch{display:inline-block;flex:none;width:.85rem;height:.85rem;border-radius:3px}.kerusi-legend__swatch--glyph{border-radius:0;overflow:visible}.kerusi-legend__occupant--selected{opacity:.2}.kerusi-legend__occupant--booked{opacity:.55}.kerusi-legend__occupant--held{opacity:.75;stroke-linecap:round;stroke-linejoin:round}.kerusi-legend__price{opacity:.7;font-variant-numeric:tabular-nums}\n"] }]
|
|
282
|
+
}], propDecorators: { legend: [{ type: i0.Input, args: [{ isSignal: true, alias: "legend", required: true }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], colors: [{ type: i0.Input, args: [{ isSignal: true, alias: "colors", required: false }] }], typeColors: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeColors", required: false }] }], seatTypeStyles: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatTypeStyles", required: false }] }], showPrices: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPrices", required: false }] }], hideUnusedTypes: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideUnusedTypes", required: false }] }], headingSeatTypes: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingSeatTypes", required: false }] }], headingAvailability: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingAvailability", required: false }] }] } });
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Marks an `<ng-template>` inside `<kerusi-seatmap>` as the drawing for every
|
|
286
|
+
* seat. The library keeps each seat's `<g>`, with its role, ARIA state, tab
|
|
287
|
+
* stop, rotation and event handlers, and stamps this template inside it.
|
|
288
|
+
*
|
|
289
|
+
* The template is declared outside any `<svg>`, so Angular cannot tell its
|
|
290
|
+
* elements are SVG: prefix each one with `svg:` (`<svg:path>`, `<svg:image>`),
|
|
291
|
+
* or it is created as an unknown HTML element and draws nothing.
|
|
292
|
+
*
|
|
293
|
+
* ```html
|
|
294
|
+
* <kerusi-seatmap [map]="map">
|
|
295
|
+
* <ng-template kerusiSeat let-seat>
|
|
296
|
+
* <ng-container *ngTemplateOutlet="seat.defaultContent; context: { $implicit: seat }" />
|
|
297
|
+
* <svg:circle [attr.cx]="seat.glyph.box.x" [attr.cy]="seat.glyph.box.y" r="3" />
|
|
298
|
+
* </ng-template>
|
|
299
|
+
* </kerusi-seatmap>
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
class KerusiSeatTemplateDirective {
|
|
303
|
+
template = inject(TemplateRef);
|
|
304
|
+
/** Types `let-seat` as a {@link SeatTemplateContext}. */
|
|
305
|
+
static ngTemplateContextGuard(_dir, ctx) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: KerusiSeatTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
309
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.2", type: KerusiSeatTemplateDirective, isStandalone: true, selector: "ng-template[kerusiSeat]", ngImport: i0 });
|
|
310
|
+
}
|
|
311
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: KerusiSeatTemplateDirective, decorators: [{
|
|
312
|
+
type: Directive,
|
|
313
|
+
args: [{ selector: 'ng-template[kerusiSeat]' }]
|
|
314
|
+
}] });
|
|
364
315
|
|
|
365
316
|
/**
|
|
366
317
|
* Renders a Kerusi seat map.
|
|
@@ -375,6 +326,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
|
|
|
375
326
|
* ```html
|
|
376
327
|
* <kerusi-seatmap [map]="map" [state]="state" [(selection)]="picked" />
|
|
377
328
|
* ```
|
|
329
|
+
*
|
|
330
|
+
* Two opt-in features sit on top, both off by default so an existing map is
|
|
331
|
+
* unchanged: `[zoomable]` (pinch, wheel and drag, keys `+` `-` `0`, buttons)
|
|
332
|
+
* and `[maximizable]` (a full-screen button). Their arithmetic is in
|
|
333
|
+
* `@kerusiweb/core`; this class supplies the listeners and the signals.
|
|
378
334
|
*/
|
|
379
335
|
class KerusiSeatmapComponent {
|
|
380
336
|
host = inject((ElementRef));
|
|
@@ -406,6 +362,25 @@ class KerusiSeatmapComponent {
|
|
|
406
362
|
/** Let an available seat take its `SeatType.color` (§4.7). */
|
|
407
363
|
typeColors = input(true, /* @ts-ignore */
|
|
408
364
|
...(ngDevMode ? [{ debugName: "typeColors" }] : /* istanbul ignore next */ []));
|
|
365
|
+
/**
|
|
366
|
+
* Styles for every seat of a type, keyed by `SeatType.id`: fill, label colour
|
|
367
|
+
* and classes. Never changes a selected or blocked seat's fill. Also shown in
|
|
368
|
+
* the legend.
|
|
369
|
+
*/
|
|
370
|
+
seatTypeStyles = input(undefined, /* @ts-ignore */
|
|
371
|
+
...(ngDevMode ? [{ debugName: "seatTypeStyles" }] : /* istanbul ignore next */ []));
|
|
372
|
+
/**
|
|
373
|
+
* A style for one seat, laid over its `seatTypeStyles` entry; return nothing
|
|
374
|
+
* to leave a seat be. Never changes a selected or blocked seat's fill.
|
|
375
|
+
*/
|
|
376
|
+
seatStyle = input(undefined, /* @ts-ignore */
|
|
377
|
+
...(ngDevMode ? [{ debugName: "seatStyle" }] : /* istanbul ignore next */ []));
|
|
378
|
+
/**
|
|
379
|
+
* A `<ng-template kerusiSeat let-seat>` placed inside the map draws the inside
|
|
380
|
+
* of every seat in place of the stock glyph. See {@link KerusiSeatTemplateDirective}.
|
|
381
|
+
*/
|
|
382
|
+
seatTemplate = contentChild(KerusiSeatTemplateDirective, /* @ts-ignore */
|
|
383
|
+
...(ngDevMode ? [{ debugName: "seatTemplate" }] : /* istanbul ignore next */ []));
|
|
409
384
|
/** Grid cell edge, in viewBox units. */
|
|
410
385
|
seatSize = input(28, /* @ts-ignore */
|
|
411
386
|
...(ngDevMode ? [{ debugName: "seatSize" }] : /* istanbul ignore next */ []));
|
|
@@ -456,6 +431,28 @@ class KerusiSeatmapComponent {
|
|
|
456
431
|
...(ngDevMode ? [{ debugName: "expireHolds" }] : /* istanbul ignore next */ []));
|
|
457
432
|
expiryIntervalMs = input(1000, /* @ts-ignore */
|
|
458
433
|
...(ngDevMode ? [{ debugName: "expiryIntervalMs" }] : /* istanbul ignore next */ []));
|
|
434
|
+
// --- zoom, pan and maximize (both opt in) ---------------------------------
|
|
435
|
+
/**
|
|
436
|
+
* Pinch, Ctrl/Cmd + wheel and drag to zoom and pan, `+` `-` `0` on the keyboard,
|
|
437
|
+
* and on-screen buttons. A tap still selects at every zoom level.
|
|
438
|
+
*/
|
|
439
|
+
zoomable = input(false, /* @ts-ignore */
|
|
440
|
+
...(ngDevMode ? [{ debugName: "zoomable" }] : /* istanbul ignore next */ []));
|
|
441
|
+
/** The zoom ceiling, relative to fit-to-container. The floor is always fit. */
|
|
442
|
+
maxZoom = input(DEFAULT_MAX_ZOOM, /* @ts-ignore */
|
|
443
|
+
...(ngDevMode ? [{ debugName: "maxZoom" }] : /* istanbul ignore next */ []));
|
|
444
|
+
/**
|
|
445
|
+
* The built-in zoom buttons. Turn this off to supply your own, anywhere on the
|
|
446
|
+
* page, and call `zoomIn()`, `zoomOut()` and `resetZoom()` on the component.
|
|
447
|
+
*/
|
|
448
|
+
showZoomControls = input(true, /* @ts-ignore */
|
|
449
|
+
...(ngDevMode ? [{ debugName: "showZoomControls" }] : /* istanbul ignore next */ []));
|
|
450
|
+
/** A button that maximizes the map to fill the screen. */
|
|
451
|
+
maximizable = input(false, /* @ts-ignore */
|
|
452
|
+
...(ngDevMode ? [{ debugName: "maximizable" }] : /* istanbul ignore next */ []));
|
|
453
|
+
/** Overrides the built-in English names of the zoom and maximize controls. */
|
|
454
|
+
controlStrings = input({}, /* @ts-ignore */
|
|
455
|
+
...(ngDevMode ? [{ debugName: "controlStrings" }] : /* istanbul ignore next */ []));
|
|
459
456
|
// --- validation -----------------------------------------------------------
|
|
460
457
|
validate = input('collect', /* @ts-ignore */
|
|
461
458
|
...(ngDevMode ? [{ debugName: "validate" }] : /* istanbul ignore next */ []));
|
|
@@ -466,6 +463,9 @@ class KerusiSeatmapComponent {
|
|
|
466
463
|
seatFocus = output();
|
|
467
464
|
/** Emitted whenever the documents change, in `collect` mode. */
|
|
468
465
|
validationIssues = output();
|
|
466
|
+
/** The zoom and pan, whenever they change. For a host that persists or shows them. */
|
|
467
|
+
viewportChange = output();
|
|
468
|
+
maximizedChange = output();
|
|
469
469
|
// --- internal state -------------------------------------------------------
|
|
470
470
|
/**
|
|
471
471
|
* The seat holding each section's tab stop, keyed by section id.
|
|
@@ -481,6 +481,25 @@ class KerusiSeatmapComponent {
|
|
|
481
481
|
/** Bumped by the hold ticker to force the state to be re-derived. */
|
|
482
482
|
expiryTick = signal(0, /* @ts-ignore */
|
|
483
483
|
...(ngDevMode ? [{ debugName: "expiryTick" }] : /* istanbul ignore next */ []));
|
|
484
|
+
viewportState = signal(FIT_VIEWPORT, /* @ts-ignore */
|
|
485
|
+
...(ngDevMode ? [{ debugName: "viewportState" }] : /* istanbul ignore next */ []));
|
|
486
|
+
maximizedState = signal(false, /* @ts-ignore */
|
|
487
|
+
...(ngDevMode ? [{ debugName: "maximizedState" }] : /* istanbul ignore next */ []));
|
|
488
|
+
/** True for a discrete change (button, key, reveal), false while a finger or wheel drives. */
|
|
489
|
+
animated = signal(false, /* @ts-ignore */
|
|
490
|
+
...(ngDevMode ? [{ debugName: "animated" }] : /* istanbul ignore next */ []));
|
|
491
|
+
viewportEl = viewChild('viewport', /* @ts-ignore */
|
|
492
|
+
...(ngDevMode ? [{ debugName: "viewportEl" }] : /* istanbul ignore next */ []));
|
|
493
|
+
contentEl = viewChild('content', /* @ts-ignore */
|
|
494
|
+
...(ngDevMode ? [{ debugName: "contentEl" }] : /* istanbul ignore next */ []));
|
|
495
|
+
gesture = IDLE_GESTURE;
|
|
496
|
+
suppressClick = false;
|
|
497
|
+
gestureBase = null;
|
|
498
|
+
nativeFullscreen = false;
|
|
499
|
+
/** The current zoom and pan. Read-only: drive it with the methods below. */
|
|
500
|
+
viewport = this.viewportState.asReadonly();
|
|
501
|
+
/** Whether the map is currently maximized. */
|
|
502
|
+
maximized = this.maximizedState.asReadonly();
|
|
484
503
|
constructor() {
|
|
485
504
|
effect(() => {
|
|
486
505
|
const issues = this.issues();
|
|
@@ -520,6 +539,93 @@ class KerusiSeatmapComponent {
|
|
|
520
539
|
const handle = setInterval(() => this.expiryTick.update((n) => n + 1), this.expiryIntervalMs());
|
|
521
540
|
onCleanup(() => clearInterval(handle));
|
|
522
541
|
});
|
|
542
|
+
// Gesture listeners go on natively, not through the template: the wheel
|
|
543
|
+
// listener must be non-passive to `preventDefault` a page zoom, and the
|
|
544
|
+
// click listener must be in the capture phase to swallow the click that
|
|
545
|
+
// follows a drag before the seat sees it.
|
|
546
|
+
effect((onCleanup) => {
|
|
547
|
+
const el = this.viewportEl()?.nativeElement;
|
|
548
|
+
if (!el) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const listeners = [
|
|
552
|
+
['pointerdown', (e) => this.onPointerDown(e), false],
|
|
553
|
+
['pointermove', (e) => this.onPointerMove(e), false],
|
|
554
|
+
['pointerup', (e) => this.onPointerEnd(e, 'up'), false],
|
|
555
|
+
['pointercancel', (e) => this.onPointerEnd(e, 'cancel'), false],
|
|
556
|
+
['wheel', (e) => this.onWheel(e), { passive: false }],
|
|
557
|
+
['click', (e) => this.onClickCapture(e), true],
|
|
558
|
+
// Safari reports a trackpad pinch as gesture events, not Ctrl + wheel.
|
|
559
|
+
['gesturestart', (e) => this.onGestureStart(e), { passive: false }],
|
|
560
|
+
['gesturechange', (e) => this.onGestureChange(e), { passive: false }],
|
|
561
|
+
['gestureend', (e) => this.onGestureEnd(e), { passive: false }],
|
|
562
|
+
];
|
|
563
|
+
for (const [type, listener, options] of listeners) {
|
|
564
|
+
el.addEventListener(type, listener, options);
|
|
565
|
+
}
|
|
566
|
+
onCleanup(() => {
|
|
567
|
+
for (const [type, listener, options] of listeners) {
|
|
568
|
+
el.removeEventListener(type, listener, options);
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
// Re-clamp when the box or the ceiling changes: a rotated phone, a
|
|
573
|
+
// maximized map, a lowered `maxZoom`.
|
|
574
|
+
effect((onCleanup) => {
|
|
575
|
+
this.limits();
|
|
576
|
+
const view = this.viewportEl()?.nativeElement;
|
|
577
|
+
untracked(() => this.reclamp());
|
|
578
|
+
if (!view || typeof ResizeObserver === 'undefined') {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const observer = new ResizeObserver(() => this.reclamp());
|
|
582
|
+
observer.observe(view);
|
|
583
|
+
const content = this.contentEl()?.nativeElement;
|
|
584
|
+
if (content) {
|
|
585
|
+
observer.observe(content);
|
|
586
|
+
}
|
|
587
|
+
onCleanup(() => observer.disconnect());
|
|
588
|
+
});
|
|
589
|
+
// Switching `zoomable` or `maximizable` off puts the map back as it was.
|
|
590
|
+
effect(() => {
|
|
591
|
+
if (!this.zoomable() && !viewportsEqual(untracked(this.viewportState), FIT_VIEWPORT)) {
|
|
592
|
+
this.viewportState.set(FIT_VIEWPORT);
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
effect(() => {
|
|
596
|
+
if (!this.maximizable() && untracked(this.maximizedState)) {
|
|
597
|
+
untracked(() => this.setMaximized(false));
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
// While maximized: freeze the page behind, and follow the browser leaving
|
|
601
|
+
// native full screen (Esc) so the two never disagree.
|
|
602
|
+
effect((onCleanup) => {
|
|
603
|
+
if (!this.maximizedState()) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const host = this.host.nativeElement;
|
|
607
|
+
const doc = host.ownerDocument;
|
|
608
|
+
const previousOverflow = doc.body.style.overflow;
|
|
609
|
+
doc.body.style.overflow = 'hidden';
|
|
610
|
+
const onFullscreenChange = () => {
|
|
611
|
+
if (doc.fullscreenElement === host) {
|
|
612
|
+
this.nativeFullscreen = true;
|
|
613
|
+
}
|
|
614
|
+
else if (this.nativeFullscreen) {
|
|
615
|
+
this.nativeFullscreen = false;
|
|
616
|
+
this.setMaximized(false);
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
doc.addEventListener('fullscreenchange', onFullscreenChange);
|
|
620
|
+
onCleanup(() => {
|
|
621
|
+
doc.body.style.overflow = previousOverflow;
|
|
622
|
+
doc.removeEventListener('fullscreenchange', onFullscreenChange);
|
|
623
|
+
this.nativeFullscreen = false;
|
|
624
|
+
if (doc.fullscreenElement === host) {
|
|
625
|
+
doc.exitFullscreen?.().catch(() => undefined);
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
});
|
|
523
629
|
}
|
|
524
630
|
// --- derived model --------------------------------------------------------
|
|
525
631
|
issues = computed(() => this.validate() === 'off'
|
|
@@ -555,6 +661,22 @@ class KerusiSeatmapComponent {
|
|
|
555
661
|
...(ngDevMode ? [{ debugName: "resolvedColors" }] : /* istanbul ignore next */ []));
|
|
556
662
|
selectionSet = computed(() => new Set(this.selection()), /* @ts-ignore */
|
|
557
663
|
...(ngDevMode ? [{ debugName: "selectionSet" }] : /* istanbul ignore next */ []));
|
|
664
|
+
limits = computed(() => zoomLimits(this.maxZoom()), /* @ts-ignore */
|
|
665
|
+
...(ngDevMode ? [{ debugName: "limits" }] : /* istanbul ignore next */ []));
|
|
666
|
+
strings = computed(() => resolveControlStrings(this.controlStrings()), /* @ts-ignore */
|
|
667
|
+
...(ngDevMode ? [{ debugName: "strings" }] : /* istanbul ignore next */ []));
|
|
668
|
+
controlColors = computed(() => controlStyle(this.colors()), /* @ts-ignore */
|
|
669
|
+
...(ngDevMode ? [{ debugName: "controlColors" }] : /* istanbul ignore next */ []));
|
|
670
|
+
transform = computed(() => viewportTransform(this.viewportState()), /* @ts-ignore */
|
|
671
|
+
...(ngDevMode ? [{ debugName: "transform" }] : /* istanbul ignore next */ []));
|
|
672
|
+
touchAction = computed(() => viewportTouchAction(this.viewportState(), this.maximizedState()), /* @ts-ignore */
|
|
673
|
+
...(ngDevMode ? [{ debugName: "touchAction" }] : /* istanbul ignore next */ []));
|
|
674
|
+
showToolbar = computed(() => (this.zoomable() && this.showZoomControls()) || this.maximizable(), /* @ts-ignore */
|
|
675
|
+
...(ngDevMode ? [{ debugName: "showToolbar" }] : /* istanbul ignore next */ []));
|
|
676
|
+
atMaxZoom = computed(() => this.viewportState().zoom >= this.limits().maxZoom - 1e-4, /* @ts-ignore */
|
|
677
|
+
...(ngDevMode ? [{ debugName: "atMaxZoom" }] : /* istanbul ignore next */ []));
|
|
678
|
+
atFit = computed(() => this.viewportState().zoom <= 1 + 1e-4, /* @ts-ignore */
|
|
679
|
+
...(ngDevMode ? [{ debugName: "atFit" }] : /* istanbul ignore next */ []));
|
|
558
680
|
/** Placed geometry per section, keyed by section id. */
|
|
559
681
|
layouts = computed(() => {
|
|
560
682
|
const overrides = this.sectionOverrides();
|
|
@@ -643,6 +765,9 @@ class KerusiSeatmapComponent {
|
|
|
643
765
|
}
|
|
644
766
|
this.setFocusedSeat(seat.sectionId, seatId);
|
|
645
767
|
this.seatFocus.emit(seat);
|
|
768
|
+
if (this.zoomable()) {
|
|
769
|
+
this.revealSeat(seatId);
|
|
770
|
+
}
|
|
646
771
|
}
|
|
647
772
|
/**
|
|
648
773
|
* Arrow keys walk the navigation graph, which orders seats by `col` in grid
|
|
@@ -714,19 +839,247 @@ class KerusiSeatmapComponent {
|
|
|
714
839
|
/**
|
|
715
840
|
* Moves the roving tab stop, then the DOM focus that follows it.
|
|
716
841
|
*
|
|
842
|
+
* When zoomable, the browser's own scroll-into-view is suppressed: the map
|
|
843
|
+
* moves by transform, so `onSeatFocused` pans it into view instead.
|
|
844
|
+
*/
|
|
845
|
+
moveFocusTo(sectionId, seatId) {
|
|
846
|
+
this.setFocusedSeat(sectionId, seatId);
|
|
847
|
+
const node = this.seatNode(seatId);
|
|
848
|
+
if (this.zoomable()) {
|
|
849
|
+
node?.focus?.({ preventScroll: true });
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
node?.focus?.();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
717
856
|
* Seat ids come from the document, so they cannot be interpolated into a
|
|
718
857
|
* selector unescaped — and `CSS.escape` is absent in jsdom and under SSR.
|
|
719
858
|
* Scanning the rendered nodes avoids both problems.
|
|
720
859
|
*/
|
|
721
|
-
|
|
722
|
-
this.setFocusedSeat(sectionId, seatId);
|
|
860
|
+
seatNode(seatId) {
|
|
723
861
|
const nodes = this.host.nativeElement.querySelectorAll('[data-seat-id]');
|
|
724
862
|
for (const node of Array.from(nodes)) {
|
|
725
863
|
if (node.dataset['seatId'] === seatId) {
|
|
726
|
-
node
|
|
864
|
+
return node;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
// --- zoom and pan ---------------------------------------------------------
|
|
870
|
+
/** Zooms in one step about the centre. No-op unless `zoomable`. */
|
|
871
|
+
zoomIn() {
|
|
872
|
+
this.stepZoom(1);
|
|
873
|
+
}
|
|
874
|
+
/** Zooms out one step about the centre. No-op unless `zoomable`. */
|
|
875
|
+
zoomOut() {
|
|
876
|
+
this.stepZoom(-1);
|
|
877
|
+
}
|
|
878
|
+
/** Back to fit-to-container: the map's original view. No-op unless `zoomable`. */
|
|
879
|
+
resetZoom() {
|
|
880
|
+
const measured = this.measure();
|
|
881
|
+
if (measured && this.zoomable()) {
|
|
882
|
+
this.commit(fitViewport(measured.content, measured.container, this.limits()), true);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
stepZoom(direction) {
|
|
886
|
+
const measured = this.measure();
|
|
887
|
+
if (measured && this.zoomable()) {
|
|
888
|
+
const next = stepViewport(this.viewportState(), direction, measured.content, measured.container, this.limits());
|
|
889
|
+
this.commit(next, true);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
/** Sizes in CSS pixels, read when needed: `offset*` ignores the zoom transform. */
|
|
893
|
+
measure() {
|
|
894
|
+
const view = this.viewportEl()?.nativeElement;
|
|
895
|
+
const content = this.contentEl()?.nativeElement;
|
|
896
|
+
if (!view || !content) {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
return {
|
|
900
|
+
container: { width: view.clientWidth, height: view.clientHeight },
|
|
901
|
+
content: { width: content.offsetWidth, height: content.offsetHeight },
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
commit(next, animate) {
|
|
905
|
+
if (viewportsEqual(next, this.viewportState())) {
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
this.animated.set(animate);
|
|
909
|
+
this.viewportState.set(next);
|
|
910
|
+
this.viewportChange.emit(next);
|
|
911
|
+
}
|
|
912
|
+
reclamp() {
|
|
913
|
+
const measured = this.measure();
|
|
914
|
+
if (measured && this.zoomable()) {
|
|
915
|
+
const next = clampViewport(this.viewportState(), measured.content, measured.container, this.limits());
|
|
916
|
+
this.commit(next, false);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
/** A point in the viewport's own coordinates, which is what the core functions take. */
|
|
920
|
+
containerPoint(event) {
|
|
921
|
+
const box = this.viewportEl().nativeElement.getBoundingClientRect();
|
|
922
|
+
return { x: event.clientX - box.left, y: event.clientY - box.top };
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Pans a seat that took keyboard focus into view. A click or tap also
|
|
926
|
+
* focuses the seat, but should not move the map under the finger, so only a
|
|
927
|
+
* `:focus-visible` focus counts.
|
|
928
|
+
*/
|
|
929
|
+
revealSeat(seatId) {
|
|
930
|
+
const view = this.viewportEl()?.nativeElement;
|
|
931
|
+
const node = this.seatNode(seatId);
|
|
932
|
+
const measured = this.measure();
|
|
933
|
+
if (!view || !node || !measured || !focusIsVisible(node)) {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
const seat = node.getBoundingClientRect();
|
|
937
|
+
const frame = view.getBoundingClientRect();
|
|
938
|
+
const next = revealRect(this.viewportState(), {
|
|
939
|
+
x: seat.left - frame.left,
|
|
940
|
+
y: seat.top - frame.top,
|
|
941
|
+
width: seat.width,
|
|
942
|
+
height: seat.height,
|
|
943
|
+
}, measured.content, measured.container, undefined, this.limits());
|
|
944
|
+
this.commit(next, true);
|
|
945
|
+
}
|
|
946
|
+
// --- pointer gestures -----------------------------------------------------
|
|
947
|
+
onPointerDown(event) {
|
|
948
|
+
if (event.pointerType === 'mouse' && event.button !== 0) {
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
this.suppressClick = false;
|
|
952
|
+
this.feedGesture({ type: 'down', id: event.pointerId, point: this.containerPoint(event) });
|
|
953
|
+
}
|
|
954
|
+
onPointerMove(event) {
|
|
955
|
+
if (this.gesture.ids.length > 0) {
|
|
956
|
+
this.feedGesture({ type: 'move', id: event.pointerId, point: this.containerPoint(event) });
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
onPointerEnd(event, type) {
|
|
960
|
+
this.feedGesture({ type, id: event.pointerId });
|
|
961
|
+
}
|
|
962
|
+
feedGesture(event) {
|
|
963
|
+
const measured = this.measure();
|
|
964
|
+
if (!measured) {
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
const result = reduceGesture(this.gesture, event, {
|
|
968
|
+
viewport: this.viewportState(),
|
|
969
|
+
content: measured.content,
|
|
970
|
+
container: measured.container,
|
|
971
|
+
limits: this.limits(),
|
|
972
|
+
});
|
|
973
|
+
this.gesture = result.state;
|
|
974
|
+
// Capture only once a tap has become a gesture: capturing on `down` would
|
|
975
|
+
// retarget the tap's click away from the seat.
|
|
976
|
+
if (result.capture) {
|
|
977
|
+
const view = this.viewportEl()?.nativeElement;
|
|
978
|
+
for (const id of result.state.ids) {
|
|
979
|
+
try {
|
|
980
|
+
view?.setPointerCapture(id);
|
|
981
|
+
}
|
|
982
|
+
catch {
|
|
983
|
+
// The pointer already ended; nothing to capture.
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
if (result.viewport !== this.viewportState()) {
|
|
988
|
+
this.commit(result.viewport, false);
|
|
989
|
+
}
|
|
990
|
+
if (result.suppressClick) {
|
|
991
|
+
// The click a drag ends with arrives right after `pointerup`, before this timer.
|
|
992
|
+
this.suppressClick = true;
|
|
993
|
+
setTimeout(() => (this.suppressClick = false));
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
onClickCapture(event) {
|
|
997
|
+
if (this.suppressClick) {
|
|
998
|
+
this.suppressClick = false;
|
|
999
|
+
event.stopPropagation();
|
|
1000
|
+
event.preventDefault();
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
onWheel(event) {
|
|
1004
|
+
const factor = wheelZoomFactor(event);
|
|
1005
|
+
const measured = this.measure();
|
|
1006
|
+
if (factor === null || !measured) {
|
|
1007
|
+
return; // A plain wheel is the page's to scroll.
|
|
1008
|
+
}
|
|
1009
|
+
event.preventDefault();
|
|
1010
|
+
const next = zoomViewportBy(this.viewportState(), factor, this.containerPoint(event), measured.content, measured.container, this.limits());
|
|
1011
|
+
this.commit(next, false);
|
|
1012
|
+
}
|
|
1013
|
+
onGestureStart(event) {
|
|
1014
|
+
event.preventDefault();
|
|
1015
|
+
this.gestureBase = this.viewportState();
|
|
1016
|
+
}
|
|
1017
|
+
onGestureChange(event) {
|
|
1018
|
+
event.preventDefault();
|
|
1019
|
+
const measured = this.measure();
|
|
1020
|
+
if (!this.gestureBase || !measured) {
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
const next = zoomViewportTo(this.gestureBase, this.gestureBase.zoom * event.scale, this.containerPoint(event), measured.content, measured.container, this.limits());
|
|
1024
|
+
this.commit(next, false);
|
|
1025
|
+
}
|
|
1026
|
+
onGestureEnd(event) {
|
|
1027
|
+
event.preventDefault();
|
|
1028
|
+
this.gestureBase = null;
|
|
1029
|
+
}
|
|
1030
|
+
// --- keyboard on the map as a whole ----------------------------------------
|
|
1031
|
+
/**
|
|
1032
|
+
* `+` `-` `0` zoom, and arrows pan when the viewport itself has focus; Esc
|
|
1033
|
+
* leaves a maximized map. A seat's own handler runs first, so an Esc that
|
|
1034
|
+
* cleared the selection is already `defaultPrevented` and stays at that.
|
|
1035
|
+
*/
|
|
1036
|
+
onRootKeydown(event) {
|
|
1037
|
+
if (event.defaultPrevented || isTextEntryTarget(event.target)) {
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
if (this.zoomable()) {
|
|
1041
|
+
const measured = this.measure();
|
|
1042
|
+
const next = measured &&
|
|
1043
|
+
keyboardViewport(this.viewportState(), {
|
|
1044
|
+
key: event.key,
|
|
1045
|
+
ctrlKey: event.ctrlKey,
|
|
1046
|
+
metaKey: event.metaKey,
|
|
1047
|
+
altKey: event.altKey,
|
|
1048
|
+
viewportFocused: event.target === this.viewportEl()?.nativeElement,
|
|
1049
|
+
}, measured.content, measured.container, this.limits());
|
|
1050
|
+
if (next) {
|
|
1051
|
+
event.preventDefault();
|
|
1052
|
+
this.commit(next, true);
|
|
727
1053
|
return;
|
|
728
1054
|
}
|
|
729
1055
|
}
|
|
1056
|
+
if (event.key === 'Escape' && this.maximizedState()) {
|
|
1057
|
+
event.preventDefault();
|
|
1058
|
+
this.setMaximized(false);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
// --- maximize -------------------------------------------------------------
|
|
1062
|
+
/** Maximizes or restores the map. Maximizing needs `maximizable`. */
|
|
1063
|
+
toggleMaximized() {
|
|
1064
|
+
this.setMaximized(!this.maximizedState());
|
|
1065
|
+
}
|
|
1066
|
+
setMaximized(next) {
|
|
1067
|
+
if (next === this.maximizedState() || (next && !this.maximizable())) {
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
this.maximizedState.set(next);
|
|
1071
|
+
this.maximizedChange.emit(next);
|
|
1072
|
+
// Native full screen is an enhancement over the fixed-position layout, which
|
|
1073
|
+
// is all iPhone Safari offers. It is requested here, inside the click that
|
|
1074
|
+
// got us here, because browsers only grant it to a user gesture.
|
|
1075
|
+
const host = this.host.nativeElement;
|
|
1076
|
+
const doc = host.ownerDocument;
|
|
1077
|
+
if (next && !doc.fullscreenElement) {
|
|
1078
|
+
host.requestFullscreen?.()?.catch(() => undefined);
|
|
1079
|
+
}
|
|
1080
|
+
else if (!next && doc.fullscreenElement === host) {
|
|
1081
|
+
doc.exitFullscreen?.()?.catch(() => undefined);
|
|
1082
|
+
}
|
|
730
1083
|
}
|
|
731
1084
|
announceSelection(selection) {
|
|
732
1085
|
const strings = this.ariaStrings();
|
|
@@ -758,15 +1111,24 @@ class KerusiSeatmapComponent {
|
|
|
758
1111
|
return (this.map().sections ?? []).map((s) => s.id).filter((id) => !hidden.includes(id));
|
|
759
1112
|
}
|
|
760
1113
|
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 });
|
|
1114
|
+
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 }, seatTypeStyles: { classPropertyName: "seatTypeStyles", publicName: "seatTypeStyles", isSignal: true, isRequired: false, transformFunction: null }, seatStyle: { classPropertyName: "seatStyle", publicName: "seatStyle", 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" }, queries: [{ propertyName: "seatTemplate", first: true, predicate: KerusiSeatTemplateDirective, descendants: true, isSignal: true }], 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 [seatTypeStyles]=\"seatTypeStyles()\"\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 [seatTemplate]=\"seatTemplate()?.template\"\r\n [seatStyle]=\"seatStyle()\"\r\n [seatTypeStyles]=\"seatTypeStyles()\"\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", "seatTypeStyles", "seatStyle", "seatTemplate", "interactive", "unitScale"], outputs: ["seatActivate", "seatFocused", "seatKeydown"] }, { kind: "component", type: KerusiLegendComponent, selector: "kerusi-legend", inputs: ["legend", "locale", "colors", "typeColors", "seatTypeStyles", "showPrices", "hideUnusedTypes", "headingSeatTypes", "headingAvailability"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
762
1115
|
}
|
|
763
1116
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: KerusiSeatmapComponent, decorators: [{
|
|
764
1117
|
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"] }] } });
|
|
1118
|
+
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 [seatTypeStyles]=\"seatTypeStyles()\"\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 [seatTemplate]=\"seatTemplate()?.template\"\r\n [seatStyle]=\"seatStyle()\"\r\n [seatTypeStyles]=\"seatTypeStyles()\"\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"] }]
|
|
1119
|
+
}], 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 }] }], seatTypeStyles: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatTypeStyles", required: false }] }], seatStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "seatStyle", required: false }] }], seatTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => KerusiSeatTemplateDirective), { isSignal: true }] }], 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
1120
|
function formatTotal(total, locale) {
|
|
768
1121
|
return total ? formatMoney(total, locale) : '';
|
|
769
1122
|
}
|
|
1123
|
+
/** `:focus-visible` where the engine knows it; otherwise assume the focus is the keyboard's. */
|
|
1124
|
+
function focusIsVisible(node) {
|
|
1125
|
+
try {
|
|
1126
|
+
return node.matches(':focus-visible');
|
|
1127
|
+
}
|
|
1128
|
+
catch {
|
|
1129
|
+
return true;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
770
1132
|
|
|
771
1133
|
/**
|
|
772
1134
|
* The Kerusi-native seat map: `<kerusi-seatmap>`, its section renderer, and an
|
|
@@ -876,5 +1238,5 @@ class KerusiStateStore {
|
|
|
876
1238
|
* Generated bundle index. Do not edit.
|
|
877
1239
|
*/
|
|
878
1240
|
|
|
879
|
-
export { KerusiLegendComponent, KerusiSeatmapComponent, KerusiSectionComponent, KerusiStateStore };
|
|
1241
|
+
export { KerusiLegendComponent, KerusiSeatTemplateDirective, KerusiSeatmapComponent, KerusiSectionComponent, KerusiStateStore };
|
|
880
1242
|
//# sourceMappingURL=kerusiweb-angular.mjs.map
|