@yuuvis/client-framework 3.17.1 → 3.19.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/fesm2022/yuuvis-client-framework-actions.mjs +2 -2
- package/fesm2022/yuuvis-client-framework-actions.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-badges.mjs +45 -7
- package/fesm2022/yuuvis-client-framework-badges.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-forms.mjs +6 -4
- package/fesm2022/yuuvis-client-framework-forms.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-object-flavor.mjs +15 -6
- package/fesm2022/yuuvis-client-framework-object-flavor.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-object-preview.mjs +25 -13
- package/fesm2022/yuuvis-client-framework-object-preview.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-renderer.mjs +81 -8
- package/fesm2022/yuuvis-client-framework-renderer.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-smart-search.mjs +385 -61
- package/fesm2022/yuuvis-client-framework-smart-search.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-tile-list.mjs +4 -4
- package/fesm2022/yuuvis-client-framework-tile-list.mjs.map +1 -1
- package/lib/assets/i18n/de.json +1 -0
- package/lib/assets/i18n/en.json +1 -0
- package/package.json +5 -5
- package/smart-search/README.md +57 -7
- package/types/yuuvis-client-framework-badges.d.ts +51 -4
- package/types/yuuvis-client-framework-object-preview.d.ts +14 -1
- package/types/yuuvis-client-framework-renderer.d.ts +23 -2
- package/types/yuuvis-client-framework-smart-search.d.ts +145 -19
- package/types/yuuvis-client-framework-tile-list.d.ts +5 -0
|
@@ -149,6 +149,20 @@ function isValuelessOperator(operator) {
|
|
|
149
149
|
operator === 'empty' ||
|
|
150
150
|
operator === 'not_empty');
|
|
151
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* The chip's full label for a condition (`"Name like foo"`). The value segment is omitted for
|
|
154
|
+
* a valueless operator and for an unset placeholder, so the chip reads cleanly instead of
|
|
155
|
+
* carrying a trailing space.
|
|
156
|
+
*
|
|
157
|
+
* Shared by the commit path and by the label re-resolution a loaded state goes through, so a
|
|
158
|
+
* restored chip is composed exactly like a freshly built one.
|
|
159
|
+
*/
|
|
160
|
+
function composeConditionLabel(fieldLabel, operator, operatorLabel, value) {
|
|
161
|
+
if (isValuelessOperator(operator))
|
|
162
|
+
return `${fieldLabel} ${operatorLabel}`;
|
|
163
|
+
const valueLabel = Array.isArray(value) ? value.join(', ') : value;
|
|
164
|
+
return valueLabel ? `${fieldLabel} ${operatorLabel} ${valueLabel}` : `${fieldLabel} ${operatorLabel}`;
|
|
165
|
+
}
|
|
152
166
|
/**
|
|
153
167
|
* Whether a condition is an *unset placeholder*: it uses a value-requiring operator
|
|
154
168
|
* (eq / like / gt / …) but carries no value yet. Such conditions are kept in the tree
|
|
@@ -322,14 +336,19 @@ const FULLTEXT_SCOPE_COLUMN = {
|
|
|
322
336
|
/**
|
|
323
337
|
* Build the whole-object full-text clause: a single `CONTAINS('term')` predicate, optionally scoped
|
|
324
338
|
* to a column and AND-ed with a type restriction. Returns `''` when the term is blank.
|
|
339
|
+
*
|
|
340
|
+
* @param fulltext The full-text unit. An empty `types` list means "all types".
|
|
341
|
+
* @param scopeTypes The types the search is scoped to (the host's `types` allow-list). When
|
|
342
|
+
* the unit picks no concrete type, "all types" means *any of these* — so the restriction is
|
|
343
|
+
* still emitted. Pass an empty list for an unscoped search: then "all types" restricts nothing.
|
|
325
344
|
*/
|
|
326
|
-
function buildFulltextClause(fulltext) {
|
|
345
|
+
function buildFulltextClause(fulltext, scopeTypes = []) {
|
|
327
346
|
const term = fulltext.term.trim();
|
|
328
347
|
if (!term)
|
|
329
348
|
return '';
|
|
330
349
|
const column = FULLTEXT_SCOPE_COLUMN[fulltext.scope];
|
|
331
350
|
const contains = column ? `${column} CONTAINS('${escapeCmis(term)}')` : `CONTAINS('${escapeCmis(term)}')`;
|
|
332
|
-
const typeCond = buildTypeClause(fulltext.types);
|
|
351
|
+
const typeCond = buildTypeClause(fulltext.types.length > 0 ? fulltext.types : scopeTypes);
|
|
333
352
|
return typeCond ? `(${typeCond} AND ${contains})` : contains;
|
|
334
353
|
}
|
|
335
354
|
/**
|
|
@@ -341,11 +360,14 @@ function buildFulltextClause(fulltext) {
|
|
|
341
360
|
* @param blocks The type blocks with their conditions.
|
|
342
361
|
* @param combinator How the top-level units combine (`AND` / `OR`).
|
|
343
362
|
* @param fulltext Optional whole-object full-text unit.
|
|
363
|
+
* @param scopeTypes The types the whole search is scoped to (the host's `types` allow-list).
|
|
364
|
+
* Applied to the full-text unit when it targets "all types"; the blocks always carry their own
|
|
365
|
+
* (already scoped) types. Empty = unscoped, i.e. "all types" restricts nothing.
|
|
344
366
|
* @returns A `SELECT * FROM system:object WHERE …` statement, or `''`.
|
|
345
367
|
*/
|
|
346
|
-
function buildCmisQuery(blocks, combinator, fulltext) {
|
|
368
|
+
function buildCmisQuery(blocks, combinator, fulltext, scopeTypes = []) {
|
|
347
369
|
const units = [];
|
|
348
|
-
const fulltextClause = fulltext ? buildFulltextClause(fulltext) : '';
|
|
370
|
+
const fulltextClause = fulltext ? buildFulltextClause(fulltext, scopeTypes) : '';
|
|
349
371
|
if (fulltextClause !== '')
|
|
350
372
|
units.push(fulltextClause);
|
|
351
373
|
for (const block of blocks) {
|
|
@@ -503,11 +525,15 @@ _('yuv.smart-search.date-preset.this-year');
|
|
|
503
525
|
_('yuv.smart-search.operator.like');
|
|
504
526
|
_('yuv.smart-search.operator.empty');
|
|
505
527
|
_('yuv.smart-search.operator.not-empty');
|
|
528
|
+
_('yuv.smart-search.operator.eq-true');
|
|
529
|
+
_('yuv.smart-search.operator.eq-false');
|
|
506
530
|
/* eslint-disable id-length */
|
|
507
531
|
const OPERATOR_LABEL_KEYS = {
|
|
508
532
|
like: 'yuv.smart-search.operator.like',
|
|
509
533
|
empty: 'yuv.smart-search.operator.empty',
|
|
510
|
-
not_empty: 'yuv.smart-search.operator.not-empty'
|
|
534
|
+
not_empty: 'yuv.smart-search.operator.not-empty',
|
|
535
|
+
eq_true: 'yuv.smart-search.operator.eq-true',
|
|
536
|
+
eq_false: 'yuv.smart-search.operator.eq-false'
|
|
511
537
|
};
|
|
512
538
|
const OPERATOR_SYMBOLS = {
|
|
513
539
|
eq: '=',
|
|
@@ -518,12 +544,24 @@ const OPERATOR_SYMBOLS = {
|
|
|
518
544
|
lte: '<='
|
|
519
545
|
};
|
|
520
546
|
/* eslint-enable id-length */
|
|
547
|
+
/** Value equality for a normalized condition value (a string or a list of strings). */
|
|
548
|
+
function sameConditionValue(left, right) {
|
|
549
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
550
|
+
return (Array.isArray(left) &&
|
|
551
|
+
Array.isArray(right) &&
|
|
552
|
+
left.length === right.length &&
|
|
553
|
+
left.every((entry, index) => entry === right[index]));
|
|
554
|
+
}
|
|
555
|
+
return left === right;
|
|
556
|
+
}
|
|
521
557
|
const DATE_PRESETS = [
|
|
522
558
|
{ id: 'today', labelKey: 'yuv.smart-search.date-preset.today' },
|
|
523
559
|
{ id: 'thisWeek', labelKey: 'yuv.smart-search.date-preset.this-week' },
|
|
524
560
|
{ id: 'thisMonth', labelKey: 'yuv.smart-search.date-preset.this-month' },
|
|
525
561
|
{ id: 'thisYear', labelKey: 'yuv.smart-search.date-preset.this-year' }
|
|
526
562
|
];
|
|
563
|
+
/** Operator id (`date:<preset>`) → translation key, so a committed preset can be re-labelled. */
|
|
564
|
+
const DATE_PRESET_LABEL_KEYS = Object.fromEntries(DATE_PRESETS.map((preset) => [`date:${preset.id}`, preset.labelKey]));
|
|
527
565
|
/**
|
|
528
566
|
* State + mutators for SmartSearch, shared between the host component and the
|
|
529
567
|
* recursive `SmartSearchGroupComponent`. Provided at the host component level
|
|
@@ -536,7 +574,10 @@ class SmartSearchEditController {
|
|
|
536
574
|
constructor() {
|
|
537
575
|
this.#system = inject(SystemService);
|
|
538
576
|
this.#translate = inject(TranslateService);
|
|
539
|
-
/**
|
|
577
|
+
/**
|
|
578
|
+
* The set of object type IDs the search is scoped to. Empty = unscoped: every
|
|
579
|
+
* searchable type of the schema is a candidate and nothing is restricted.
|
|
580
|
+
*/
|
|
540
581
|
this.allowedTypes = signal([], ...(ngDevMode ? [{ debugName: "allowedTypes" }] : /* istanbul ignore next */ []));
|
|
541
582
|
/** ObjectTypeField IDs to exclude from the field-step autocomplete suggestions. */
|
|
542
583
|
this.skipProperties = signal([], ...(ngDevMode ? [{ debugName: "skipProperties" }] : /* istanbul ignore next */ []));
|
|
@@ -579,6 +620,12 @@ class SmartSearchEditController {
|
|
|
579
620
|
this.fulltext = signal({ term: '', scope: 'all', types: [] }, ...(ngDevMode ? [{ debugName: "fulltext" }] : /* istanbul ignore next */ []));
|
|
580
621
|
/** Whether the dynamic-conditions feature is enabled (mirrors the host `supportDynamicConditions` input). */
|
|
581
622
|
this.supportDynamic = signal(false, ...(ngDevMode ? [{ debugName: "supportDynamic" }] : /* istanbul ignore next */ []));
|
|
623
|
+
/**
|
|
624
|
+
* Whether the host renders a plain full-text search (mirrors its `fulltextOnly` input).
|
|
625
|
+
* The condition builder is hidden and {@link cmisQuery} is built from {@link fulltext}
|
|
626
|
+
* alone, so blocks a loaded state carries can't contribute invisible clauses.
|
|
627
|
+
*/
|
|
628
|
+
this.fulltextOnly = signal(false, ...(ngDevMode ? [{ debugName: "fulltextOnly" }] : /* istanbul ignore next */ []));
|
|
582
629
|
/**
|
|
583
630
|
* Form mode: replace the builder with a generated form of the user-marked dynamic
|
|
584
631
|
* conditions. View state only — the template ({@link blocks}) is never mutated; the
|
|
@@ -599,6 +646,20 @@ class SmartSearchEditController {
|
|
|
599
646
|
/** Whether any condition is marked dynamic (gates the form-mode toggle). */
|
|
600
647
|
this.hasDynamicConditions = computed(() => this.blocks().some((block) => this.#anyDynamic(block.conditions)), ...(ngDevMode ? [{ debugName: "hasDynamicConditions" }] : /* istanbul ignore next */ []));
|
|
601
648
|
this.objectTypes = computed(() => this.#system.getObjectTypes(true, 'search').filter((type) => this.allowedTypes().includes(type.id)), ...(ngDevMode ? [{ debugName: "objectTypes" }] : /* istanbul ignore next */ []));
|
|
649
|
+
/**
|
|
650
|
+
* The types an "all types" pick resolves to. With a configured allow-list, "all types"
|
|
651
|
+
* means *any of the allowed types* — not every type of the system — so the restriction
|
|
652
|
+
* is still emitted. Without one the list is empty and "all types" restricts nothing.
|
|
653
|
+
*
|
|
654
|
+
* Resolved id-by-id against the schema (rather than from {@link objectTypes}) so an
|
|
655
|
+
* allowed id the current schema doesn't know still narrows the query instead of silently
|
|
656
|
+
* widening it; `isSot` decides whether the id lands in the primary or the secondary
|
|
657
|
+
* object-type clause.
|
|
658
|
+
*/
|
|
659
|
+
this.scopeTypes = computed(() => this.allowedTypes().map((id) => {
|
|
660
|
+
const known = this.#system.getObjectType(id, true);
|
|
661
|
+
return { id, label: known?.label ?? id, isSot: known?.isSot };
|
|
662
|
+
}), ...(ngDevMode ? [{ debugName: "scopeTypes" }] : /* istanbul ignore next */ []));
|
|
602
663
|
/** Fields for the active top-level block, used by the field-step suggestions. */
|
|
603
664
|
this.activeBlockFields = computed(() => this.#sharedFields(this.activeBlock()?.types ?? []), ...(ngDevMode ? [{ debugName: "activeBlockFields" }] : /* istanbul ignore next */ []));
|
|
604
665
|
/**
|
|
@@ -652,10 +713,13 @@ class SmartSearchEditController {
|
|
|
652
713
|
this.unitCount = computed(() => (this.fulltext().term.trim() ? 1 : 0) + this.blocks().length, ...(ngDevMode ? [{ debugName: "unitCount" }] : /* istanbul ignore next */ []));
|
|
653
714
|
this.showCombinator = computed(() => this.unitCount() >= 2, ...(ngDevMode ? [{ debugName: "showCombinator" }] : /* istanbul ignore next */ []));
|
|
654
715
|
this.cmisQuery = computed(() => {
|
|
716
|
+
// Full-text only: the blocks are not rendered, so they must not be queried either.
|
|
717
|
+
if (this.fulltextOnly())
|
|
718
|
+
return buildCmisQuery([], this.combinator(), this.fulltext(), this.scopeTypes());
|
|
655
719
|
// In form mode, overlay the form's entered values onto the (untouched) template so
|
|
656
720
|
// the emitted query reflects the user's fill-out without mutating the saved blocks.
|
|
657
721
|
const blocks = this.formMode() ? overlayConditionValues(this.blocks(), this.#formValues()) : this.blocks();
|
|
658
|
-
return buildCmisQuery(blocks, this.combinator(), this.fulltext());
|
|
722
|
+
return buildCmisQuery(blocks, this.combinator(), this.fulltext(), this.scopeTypes());
|
|
659
723
|
}, ...(ngDevMode ? [{ debugName: "cmisQuery" }] : /* istanbul ignore next */ []));
|
|
660
724
|
/** The active input control for the current step. */
|
|
661
725
|
this.activeCtrl = computed(() => {
|
|
@@ -688,7 +752,16 @@ class SmartSearchEditController {
|
|
|
688
752
|
fulltext: JSON.parse(JSON.stringify(this.fulltext()))
|
|
689
753
|
};
|
|
690
754
|
}
|
|
691
|
-
/**
|
|
755
|
+
/**
|
|
756
|
+
* Replace the current state with a saved one, normalizing legacy blocks and cancelling any
|
|
757
|
+
* in-progress edit.
|
|
758
|
+
*
|
|
759
|
+
* Everything a persisted state carries for display only — type, field and operator labels and
|
|
760
|
+
* the composed condition label — is re-resolved against the live schema and the *current* UI
|
|
761
|
+
* language (see {@link #resolveType} / {@link #relabelNodes}), so a query saved in one language
|
|
762
|
+
* reads in the language it is restored in. The persisted strings act as the fallback for ids the
|
|
763
|
+
* schema or the translations can no longer resolve.
|
|
764
|
+
*/
|
|
692
765
|
loadState(state) {
|
|
693
766
|
this.cancelPending();
|
|
694
767
|
// Form-mode overlay keys reference the old tree; drop form mode on reload.
|
|
@@ -696,7 +769,7 @@ class SmartSearchEditController {
|
|
|
696
769
|
this.blocks.set(state.blocks.map((block) => this.#normalizeBlock(block)));
|
|
697
770
|
this.combinator.set(state.combinator);
|
|
698
771
|
const fulltext = state.fulltext ?? { term: '', scope: 'all', types: [] };
|
|
699
|
-
this.fulltext.set({ ...fulltext, types: fulltext.types.map((type) => this.#
|
|
772
|
+
this.fulltext.set({ ...fulltext, types: fulltext.types.map((type) => this.#resolveType(type)) });
|
|
700
773
|
}
|
|
701
774
|
/** Reset all search state back to its initial empty values. */
|
|
702
775
|
reset() {
|
|
@@ -764,9 +837,27 @@ class SmartSearchEditController {
|
|
|
764
837
|
else
|
|
765
838
|
this.enterFormMode();
|
|
766
839
|
}
|
|
767
|
-
/**
|
|
840
|
+
/**
|
|
841
|
+
* Record a value entered in the form for a dynamic condition (drives the overlaid
|
|
842
|
+
* {@link cmisQuery}). No-op when unchanged, so re-applying a row's current value —
|
|
843
|
+
* e.g. flushing the debounced controls on submit — can't churn the query.
|
|
844
|
+
*/
|
|
768
845
|
setFormValue(condition, raw) {
|
|
769
|
-
|
|
846
|
+
const next = normalizeConditionValue(raw);
|
|
847
|
+
const current = this.#formValues().get(condition);
|
|
848
|
+
if (current !== undefined && sameConditionValue(current, next))
|
|
849
|
+
return;
|
|
850
|
+
this.#formValues.update((values) => new Map(values).set(condition, next));
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Re-apply every generated form row's current control value. The row controls are
|
|
854
|
+
* debounced, so a submit fired immediately after typing would otherwise read a stale
|
|
855
|
+
* overlay. Rows whose value already matches are skipped by {@link setFormValue}.
|
|
856
|
+
*/
|
|
857
|
+
flushFormValues() {
|
|
858
|
+
for (const field of this.formFields()) {
|
|
859
|
+
this.setFormValue(field.condition, field.control.value);
|
|
860
|
+
}
|
|
770
861
|
}
|
|
771
862
|
/** Whether any condition in `nodes` (recursively) is marked dynamic and has a fillable value. */
|
|
772
863
|
#anyDynamic(nodes) {
|
|
@@ -782,8 +873,10 @@ class SmartSearchEditController {
|
|
|
782
873
|
return this.resolveFieldDefinition(block.types, fieldId);
|
|
783
874
|
}
|
|
784
875
|
// ── Full-text unit ────────────────────────────────────────────────────────
|
|
785
|
-
/** Set the full-text search term. */
|
|
876
|
+
/** Set the full-text search term. No-op when unchanged, so re-setting it can't churn {@link cmisQuery}. */
|
|
786
877
|
setFulltextTerm(term) {
|
|
878
|
+
if (this.fulltext().term === term)
|
|
879
|
+
return;
|
|
787
880
|
this.fulltext.update((current) => ({ ...current, term }));
|
|
788
881
|
}
|
|
789
882
|
/** Set the full-text search scope (`all` / `metadata` / `content`). */
|
|
@@ -957,7 +1050,12 @@ class SmartSearchEditController {
|
|
|
957
1050
|
this.pendingField.set(fieldItem);
|
|
958
1051
|
this.pendingOperatorLabel.set(this.operatorLabel(condition.operator));
|
|
959
1052
|
this.fieldCtrl.setValue(condition.fieldLabel, { emitEvent: false });
|
|
960
|
-
|
|
1053
|
+
// The operator is shown as a pill (from pendingOperatorLabel), so its control stays
|
|
1054
|
+
// empty: it is the filter box for picking a *replacement*, not a text rendition of
|
|
1055
|
+
// the current operator. Prefilling it would both make the label look editable and
|
|
1056
|
+
// filter the operator suggestions down to itself.
|
|
1057
|
+
this.operatorCtrl.setValue('', { emitEvent: false });
|
|
1058
|
+
this.inputTerm.set('');
|
|
961
1059
|
this.step.set(isValuelessOperator(condition.operator) ? 'operator' : 'value');
|
|
962
1060
|
}
|
|
963
1061
|
/** Jump back to the field step and clear the input so the user can re-type. */
|
|
@@ -1054,14 +1152,25 @@ class SmartSearchEditController {
|
|
|
1054
1152
|
operatorsForField(field) {
|
|
1055
1153
|
return this.#operatorsForInternalType(field.internalType ?? 'string');
|
|
1056
1154
|
}
|
|
1057
|
-
/**
|
|
1155
|
+
/**
|
|
1156
|
+
* Human-readable label for an operator id: a math symbol (`=`, `≠`, …), a translated
|
|
1157
|
+
* key, a translated date-preset label (`date:thisMonth` → “This month”) or the id itself.
|
|
1158
|
+
*/
|
|
1058
1159
|
operatorLabel(operator) {
|
|
1059
1160
|
const symbol = OPERATOR_SYMBOLS[operator];
|
|
1060
1161
|
if (symbol)
|
|
1061
1162
|
return symbol;
|
|
1062
|
-
const key = OPERATOR_LABEL_KEYS[operator];
|
|
1163
|
+
const key = OPERATOR_LABEL_KEYS[operator] ?? DATE_PRESET_LABEL_KEYS[operator];
|
|
1063
1164
|
return key ? this.#translate.instant(key) : operator;
|
|
1064
1165
|
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Whether {@link operatorLabel} can produce a real label for an operator id, i.e. whether it is
|
|
1168
|
+
* one the builder offers. `false` for anything externally authored, where `operatorLabel` falls
|
|
1169
|
+
* back to echoing the id.
|
|
1170
|
+
*/
|
|
1171
|
+
#isKnownOperator(operator) {
|
|
1172
|
+
return operator in OPERATOR_SYMBOLS || operator in OPERATOR_LABEL_KEYS || operator in DATE_PRESET_LABEL_KEYS;
|
|
1173
|
+
}
|
|
1065
1174
|
/** Build a condition record from a date preset / boolean operator / value. */
|
|
1066
1175
|
buildCommitCondition(field, operatorId, operatorLabelText, value, internalTypeOverride) {
|
|
1067
1176
|
if (operatorId.startsWith('date:')) {
|
|
@@ -1072,7 +1181,7 @@ class SmartSearchEditController {
|
|
|
1072
1181
|
operator: operatorId,
|
|
1073
1182
|
operatorLabel: operatorLabelText,
|
|
1074
1183
|
value: operatorId.slice(DATE_PREFIX_LEN$1),
|
|
1075
|
-
conditionLabel:
|
|
1184
|
+
conditionLabel: composeConditionLabel(field.label, operatorId, operatorLabelText, '')
|
|
1076
1185
|
};
|
|
1077
1186
|
}
|
|
1078
1187
|
if (operatorId === 'empty' || operatorId === 'not_empty') {
|
|
@@ -1083,7 +1192,7 @@ class SmartSearchEditController {
|
|
|
1083
1192
|
operator: operatorId,
|
|
1084
1193
|
operatorLabel: operatorLabelText,
|
|
1085
1194
|
value: '',
|
|
1086
|
-
conditionLabel:
|
|
1195
|
+
conditionLabel: composeConditionLabel(field.label, operatorId, operatorLabelText, '')
|
|
1087
1196
|
};
|
|
1088
1197
|
}
|
|
1089
1198
|
if (operatorId === 'eq_true' || operatorId === 'eq_false') {
|
|
@@ -1095,10 +1204,9 @@ class SmartSearchEditController {
|
|
|
1095
1204
|
operator: 'eq',
|
|
1096
1205
|
operatorLabel: '=',
|
|
1097
1206
|
value: val,
|
|
1098
|
-
conditionLabel:
|
|
1207
|
+
conditionLabel: composeConditionLabel(field.label, 'eq', '=', val)
|
|
1099
1208
|
};
|
|
1100
1209
|
}
|
|
1101
|
-
const valueLabel = Array.isArray(value) ? value.join(', ') : value;
|
|
1102
1210
|
return {
|
|
1103
1211
|
fieldId: field.id,
|
|
1104
1212
|
internalType: internalTypeOverride ?? field.internalType ?? 'string',
|
|
@@ -1106,11 +1214,7 @@ class SmartSearchEditController {
|
|
|
1106
1214
|
operator: operatorId,
|
|
1107
1215
|
operatorLabel: operatorLabelText,
|
|
1108
1216
|
value,
|
|
1109
|
-
|
|
1110
|
-
// cleanly ("Name like") instead of carrying a trailing space.
|
|
1111
|
-
conditionLabel: valueLabel
|
|
1112
|
-
? `${field.label} ${operatorLabelText} ${valueLabel}`
|
|
1113
|
-
: `${field.label} ${operatorLabelText}`
|
|
1217
|
+
conditionLabel: composeConditionLabel(field.label, operatorId, operatorLabelText, value)
|
|
1114
1218
|
};
|
|
1115
1219
|
}
|
|
1116
1220
|
/**
|
|
@@ -1165,9 +1269,9 @@ class SmartSearchEditController {
|
|
|
1165
1269
|
this.#blockSeq = Math.max(this.#blockSeq, Number(match[1]));
|
|
1166
1270
|
}
|
|
1167
1271
|
/**
|
|
1168
|
-
* Normalize a block coming from a loaded state. Ensures an `id`,
|
|
1169
|
-
*
|
|
1170
|
-
*
|
|
1272
|
+
* Normalize a block coming from a loaded state. Ensures an `id`, migrates legacy
|
|
1273
|
+
* single-type blocks (`typeId`/`typeLabel`/`isSot`) into the `types[]` shape so older
|
|
1274
|
+
* saved states remain loadable, and re-resolves every display label the state carried.
|
|
1171
1275
|
*/
|
|
1172
1276
|
#normalizeBlock(block) {
|
|
1173
1277
|
const legacy = block;
|
|
@@ -1178,23 +1282,72 @@ class SmartSearchEditController {
|
|
|
1178
1282
|
this.#trackSeq(id);
|
|
1179
1283
|
return {
|
|
1180
1284
|
id,
|
|
1181
|
-
types: types.map((type) => this.#
|
|
1182
|
-
conditions: legacy.conditions ?? [],
|
|
1285
|
+
types: types.map((type) => this.#resolveType(type)),
|
|
1286
|
+
conditions: this.#relabelNodes(legacy.conditions ?? []),
|
|
1183
1287
|
conditionCombinator: legacy.conditionCombinator ?? 'AND'
|
|
1184
1288
|
};
|
|
1185
1289
|
}
|
|
1186
1290
|
/**
|
|
1187
|
-
* Re-derive a type's `isSot` flag from the live schema.
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
1191
|
-
*
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
1291
|
+
* Re-derive a type's display label and its `isSot` flag from the live schema.
|
|
1292
|
+
*
|
|
1293
|
+
* `isSot` drives whether the type restriction is emitted as `objectTypeId = …` or
|
|
1294
|
+
* `system:secondaryObjectTypeIds IN (…)`, so it must reflect the schema's current
|
|
1295
|
+
* classification rather than whatever was carried in by a persisted state (legacy saved
|
|
1296
|
+
* queries predate the flag, and externally-constructed types may omit it). Resolved via the
|
|
1297
|
+
* schema (not the `allowedTypes`-filtered list) so a saved type outside the current allow-list
|
|
1298
|
+
* is still corrected; unknown ids keep their flag untouched.
|
|
1299
|
+
*
|
|
1300
|
+
* The label is presentation only, but a persisted one freezes the language the state was saved
|
|
1301
|
+
* in — so it is re-resolved the same way the type suggestions are built, falling back to the
|
|
1302
|
+
* persisted label for an id the current translations don't cover.
|
|
1194
1303
|
*/
|
|
1195
|
-
#
|
|
1304
|
+
#resolveType(type) {
|
|
1305
|
+
const resolved = { ...type, label: this.#resolveLabel(type.id, type.label) };
|
|
1196
1306
|
const known = this.#system.getObjectType(type.id);
|
|
1197
|
-
return known ? { ...
|
|
1307
|
+
return known ? { ...resolved, isSot: known.isSot } : resolved;
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Localized label for a schema id (object type, field or table column), falling back to the
|
|
1311
|
+
* label a persisted state carried and finally to the bare id. Mirrors the resolution the
|
|
1312
|
+
* suggestion lists use ({@link #sharedFields} / {@link #columnSuggestions}), so a restored chip
|
|
1313
|
+
* reads like a freshly built one.
|
|
1314
|
+
*/
|
|
1315
|
+
#resolveLabel(id, persisted) {
|
|
1316
|
+
return this.#system.getLocalizedLabel(id) || persisted || id;
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Re-resolve the display labels of a condition tree coming from a loaded state, preserving its
|
|
1320
|
+
* structure and everything that drives the query (`fieldId`, `operator`, `value`, `dynamic`).
|
|
1321
|
+
* Recurses through groups and table conditions.
|
|
1322
|
+
*/
|
|
1323
|
+
#relabelNodes(nodes) {
|
|
1324
|
+
return nodes.map((node) => {
|
|
1325
|
+
if (isTableCondition(node)) {
|
|
1326
|
+
return {
|
|
1327
|
+
...node,
|
|
1328
|
+
fieldLabel: this.#resolveLabel(node.fieldId, node.fieldLabel),
|
|
1329
|
+
conditions: this.#relabelNodes(node.conditions)
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
if (isConditionGroup(node))
|
|
1333
|
+
return { ...node, conditions: this.#relabelNodes(node.conditions) };
|
|
1334
|
+
return this.#relabelCondition(node);
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
/** Re-resolve a single condition's field / operator / chip labels (see {@link #relabelNodes}). */
|
|
1338
|
+
#relabelCondition(condition) {
|
|
1339
|
+
const fieldLabel = this.#resolveLabel(condition.fieldId, condition.fieldLabel);
|
|
1340
|
+
// For an operator the builder doesn't know, operatorLabel() would echo the raw id — keep the
|
|
1341
|
+
// persisted label instead.
|
|
1342
|
+
const operatorLabel = this.#isKnownOperator(condition.operator)
|
|
1343
|
+
? this.operatorLabel(condition.operator)
|
|
1344
|
+
: condition.operatorLabel || condition.operator;
|
|
1345
|
+
return {
|
|
1346
|
+
...condition,
|
|
1347
|
+
fieldLabel,
|
|
1348
|
+
operatorLabel,
|
|
1349
|
+
conditionLabel: composeConditionLabel(fieldLabel, condition.operator, operatorLabel, condition.value)
|
|
1350
|
+
};
|
|
1198
1351
|
}
|
|
1199
1352
|
/**
|
|
1200
1353
|
* Fields shared by *all* given types (the intersection by field id), shaped as
|
|
@@ -1318,11 +1471,7 @@ class SmartSearchEditController {
|
|
|
1318
1471
|
];
|
|
1319
1472
|
case 'boolean':
|
|
1320
1473
|
case 'boolean:switch':
|
|
1321
|
-
return [
|
|
1322
|
-
{ kind: 'operator', id: 'eq_true', label: this.#translate.instant('yuv.smart-search.operator.eq-true') },
|
|
1323
|
-
{ kind: 'operator', id: 'eq_false', label: this.#translate.instant('yuv.smart-search.operator.eq-false') },
|
|
1324
|
-
...empty()
|
|
1325
|
-
];
|
|
1474
|
+
return [...base(['eq_true', 'eq_false']), ...empty()];
|
|
1326
1475
|
case 'string:catalog':
|
|
1327
1476
|
case 'string:catalog:i18n':
|
|
1328
1477
|
case 'string:catalog:dynamic':
|
|
@@ -1453,6 +1602,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
1453
1602
|
|
|
1454
1603
|
const DEBOUNCE_MS = 150;
|
|
1455
1604
|
const DATE_PREFIX_LEN = 5;
|
|
1605
|
+
/** `keyCode` browsers report while an IME composition session is active. */
|
|
1606
|
+
const IME_KEY_CODE = 229;
|
|
1456
1607
|
/**
|
|
1457
1608
|
* Visual query builder that turns guided, chip-based user input into a CMIS query.
|
|
1458
1609
|
*
|
|
@@ -1462,15 +1613,26 @@ const DATE_PREFIX_LEN = 5;
|
|
|
1462
1613
|
* - one or more **type blocks**, each targeting one or more object types and holding
|
|
1463
1614
|
* field conditions, nested groups and table-column conditions.
|
|
1464
1615
|
*
|
|
1616
|
+
* Set {@link fulltextOnly} to drop the second part entirely and render a plain full-text
|
|
1617
|
+
* search bar.
|
|
1618
|
+
*
|
|
1465
1619
|
* Conditions are built step by step (type → field → operator → value) with an inline
|
|
1466
1620
|
* autocomplete editor; the value step renders the field's real metadata widget
|
|
1467
1621
|
* (datepicker, catalog select, organization picker, …). The resulting CMIS query is
|
|
1468
1622
|
* emitted through {@link queryChange} on every change and can be saved/restored as a
|
|
1469
|
-
* plain-data {@link SmartSearchState} via {@link getState} / {@link loadState}.
|
|
1623
|
+
* plain-data {@link SmartSearchState} via {@link getState} / {@link loadState}. A restored
|
|
1624
|
+
* state is re-labelled in the current UI language (see {@link loadState}).
|
|
1470
1625
|
*
|
|
1471
1626
|
* State and mutators live in {@link SmartSearchEditController} (provided per instance);
|
|
1472
1627
|
* this component owns only the UI concerns (focus, autocomplete plumbing, blur handling).
|
|
1473
1628
|
*
|
|
1629
|
+
* **Submitting.** {@link queryChange} is a live preview — it fires on every change. To run
|
|
1630
|
+
* a search only when the user is done, bind {@link querySubmit}: it emits when ENTER
|
|
1631
|
+
* reaches the component without an inner widget having claimed it, the way a plain HTML
|
|
1632
|
+
* form submits on ENTER. Widgets that own ENTER for their own purpose keep it: the
|
|
1633
|
+
* suggestion autocomplete picks an option, a catalog select picks a value, and the inline
|
|
1634
|
+
* editor commits the condition it is building. Only the *next* ENTER then submits.
|
|
1635
|
+
*
|
|
1474
1636
|
* @example
|
|
1475
1637
|
* ```html
|
|
1476
1638
|
* <!-- Restrict the picker to two object types and skip a noisy property -->
|
|
@@ -1509,9 +1671,14 @@ class SmartSearchComponent {
|
|
|
1509
1671
|
this.#host = inject((ElementRef));
|
|
1510
1672
|
this.ctrl = inject(SmartSearchEditController);
|
|
1511
1673
|
/**
|
|
1512
|
-
* Object-type ids that may be searched
|
|
1513
|
-
* full-text type filter and the type-block multi-select)
|
|
1514
|
-
*
|
|
1674
|
+
* Object-type ids that may be searched — the scope of the whole search. Restricts
|
|
1675
|
+
* the type picker (both the full-text type filter and the type-block multi-select)
|
|
1676
|
+
* to these types, and narrows the emitted query: picking *All types* in the
|
|
1677
|
+
* full-text filter then means "any of these types" (`objectTypeId IN (…)`), not
|
|
1678
|
+
* every type the system knows.
|
|
1679
|
+
*
|
|
1680
|
+
* When empty the search is unscoped — *All types* restricts nothing — and no type
|
|
1681
|
+
* is offered in the picker, so only a full-text search can be built.
|
|
1515
1682
|
*/
|
|
1516
1683
|
this.types = input([], ...(ngDevMode ? [{ debugName: "types" }] : /* istanbul ignore next */ []));
|
|
1517
1684
|
/**
|
|
@@ -1520,6 +1687,14 @@ class SmartSearchComponent {
|
|
|
1520
1687
|
* table columns alike.
|
|
1521
1688
|
*/
|
|
1522
1689
|
this.skipProperties = input([], ...(ngDevMode ? [{ debugName: "skipProperties" }] : /* istanbul ignore next */ []));
|
|
1690
|
+
/**
|
|
1691
|
+
* Renders the component as a plain full-text search: only the full-text bar (term, scope
|
|
1692
|
+
* and the object-type filter) is shown — no type blocks, no condition builder, no
|
|
1693
|
+
* form-mode toggle. The emitted query is built from the full-text unit alone, so a
|
|
1694
|
+
* previously {@link loadState loaded} state's blocks do not contribute while this is set.
|
|
1695
|
+
* {@link types} keeps its meaning: the search stays scoped to those types.
|
|
1696
|
+
*/
|
|
1697
|
+
this.fulltextOnly = input(false, ...(ngDevMode ? [{ debugName: "fulltextOnly" }] : /* istanbul ignore next */ []));
|
|
1523
1698
|
/**
|
|
1524
1699
|
* Enables the **dynamic conditions** feature. When `true`, each committed condition can
|
|
1525
1700
|
* be marked dynamic and a form-mode toggle appears that swaps the builder for a generated
|
|
@@ -1534,6 +1709,14 @@ class SmartSearchComponent {
|
|
|
1534
1709
|
* non-empty values.
|
|
1535
1710
|
*/
|
|
1536
1711
|
this.queryChange = output();
|
|
1712
|
+
/**
|
|
1713
|
+
* Emits the current CMIS query when the user presses ENTER and no inner widget
|
|
1714
|
+
* claimed that key — the smart-search equivalent of submitting a form. Bind this
|
|
1715
|
+
* (rather than {@link queryChange}) to run the search only once the user is done
|
|
1716
|
+
* building it. Like {@link queryChange}, `''` means "no query"; an empty search
|
|
1717
|
+
* still submits.
|
|
1718
|
+
*/
|
|
1719
|
+
this.querySubmit = output();
|
|
1537
1720
|
this.auto = viewChild.required('auto');
|
|
1538
1721
|
/** Trigger of the currently-focused autocomplete input — used to re-open the
|
|
1539
1722
|
* panel after a type pick so the multi-select stays open. */
|
|
@@ -1542,6 +1725,14 @@ class SmartSearchComponent {
|
|
|
1542
1725
|
this.fulltextTermCtrl = new FormControl('');
|
|
1543
1726
|
/** Sentinel option value representing "no type restriction" in the type multi-select. */
|
|
1544
1727
|
this.ALL_TYPES = '__all__';
|
|
1728
|
+
/**
|
|
1729
|
+
* Empty `matChipInputSeparatorKeyCodes` for the add-type input. The directive defaults
|
|
1730
|
+
* to `[ENTER]` and then calls `preventDefault()` on *every* Enter to end a chip — but
|
|
1731
|
+
* we never bind `matChipInputTokenEnd`, so that only served to hide the key from
|
|
1732
|
+
* {@link onHostEnter}. A stable reference: a `[]` literal in the template would be a
|
|
1733
|
+
* new array on each change-detection run.
|
|
1734
|
+
*/
|
|
1735
|
+
this.NO_SEPARATOR_KEYS = [];
|
|
1545
1736
|
/**
|
|
1546
1737
|
* Guard that prevents `onInlineBlur` from cancelling the pending condition
|
|
1547
1738
|
* when we programmatically open the inline editor.
|
|
@@ -1630,6 +1821,17 @@ class SmartSearchComponent {
|
|
|
1630
1821
|
effect(() => {
|
|
1631
1822
|
this.ctrl.skipProperties.set(this.skipProperties());
|
|
1632
1823
|
});
|
|
1824
|
+
// Mirror the full-text-only opt-in into the controller (it also drops the blocks from
|
|
1825
|
+
// the emitted query). Turning it on discards an in-progress condition edit and form
|
|
1826
|
+
// mode, so flipping the flag at runtime can't leave hidden UI state active.
|
|
1827
|
+
effect(() => {
|
|
1828
|
+
const only = this.fulltextOnly();
|
|
1829
|
+
this.ctrl.fulltextOnly.set(only);
|
|
1830
|
+
if (only) {
|
|
1831
|
+
this.ctrl.cancelPending();
|
|
1832
|
+
this.ctrl.exitFormMode();
|
|
1833
|
+
}
|
|
1834
|
+
});
|
|
1633
1835
|
// Mirror the dynamic-conditions opt-in into the controller; leaving the feature
|
|
1634
1836
|
// disabled also forces form mode off.
|
|
1635
1837
|
effect(() => {
|
|
@@ -1708,7 +1910,15 @@ class SmartSearchComponent {
|
|
|
1708
1910
|
getState() {
|
|
1709
1911
|
return this.ctrl.getState();
|
|
1710
1912
|
}
|
|
1711
|
-
/**
|
|
1913
|
+
/**
|
|
1914
|
+
* Restore a previously {@link getState saved} search, replacing the current one.
|
|
1915
|
+
*
|
|
1916
|
+
* The display labels a state carries (type, field and operator labels and the chip's composed
|
|
1917
|
+
* condition label) are re-resolved against the live schema and the *current* UI language, so a
|
|
1918
|
+
* search saved in one language reads in the language it is restored in. Ids the schema or the
|
|
1919
|
+
* translations can no longer resolve keep the label the state carried. Nothing that drives the
|
|
1920
|
+
* emitted query is affected.
|
|
1921
|
+
*/
|
|
1712
1922
|
loadState(state) {
|
|
1713
1923
|
this.ctrl.loadState(state);
|
|
1714
1924
|
// The full-text term input is a local control, not bound to the signal (scope and
|
|
@@ -1882,37 +2092,103 @@ class SmartSearchComponent {
|
|
|
1882
2092
|
* pick, so Enter commits the staged types — even while the autocomplete panel
|
|
1883
2093
|
* is open. While the user is typing a filter term, Enter is left to the
|
|
1884
2094
|
* autocomplete so it can select the highlighted option.
|
|
2095
|
+
*
|
|
2096
|
+
* Unlike {@link onEnter} this deliberately lets an unusable Enter fall through:
|
|
2097
|
+
* the add-type input is the resting state of an empty search, so Enter with
|
|
2098
|
+
* nothing staged has to reach {@link onHostEnter} and submit.
|
|
1885
2099
|
*/
|
|
1886
|
-
onTypeEnter() {
|
|
2100
|
+
onTypeEnter(event) {
|
|
2101
|
+
if (event.defaultPrevented)
|
|
2102
|
+
return;
|
|
1887
2103
|
// The ENTER that selected an autocomplete option must not also confirm the
|
|
1888
2104
|
// block — regardless of whether this handler runs before or after the
|
|
1889
2105
|
// autocomplete closes its panel.
|
|
1890
2106
|
if (this._suppressTypeConfirm)
|
|
1891
2107
|
return;
|
|
2108
|
+
if (this.auto().isOpen)
|
|
2109
|
+
return;
|
|
1892
2110
|
const term = this.ctrl.fieldCtrl.value;
|
|
1893
2111
|
if (typeof term === 'string' && term.trim())
|
|
1894
2112
|
return;
|
|
1895
2113
|
if (this.ctrl.draftTypes().length === 0)
|
|
1896
2114
|
return;
|
|
2115
|
+
// Claim the key so it doesn't also submit the search.
|
|
2116
|
+
event.preventDefault();
|
|
1897
2117
|
this.confirmTypes();
|
|
1898
2118
|
}
|
|
1899
2119
|
/**
|
|
1900
|
-
* Enter / confirm-button handler for the
|
|
1901
|
-
* condition
|
|
1902
|
-
* (Enter selects the highlighted option there)
|
|
2120
|
+
* Enter / confirm-button handler for the inline editor. Commits the in-progress
|
|
2121
|
+
* condition once it has a field and an operator. No-op while the autocomplete
|
|
2122
|
+
* panel is open (Enter selects the highlighted option there).
|
|
2123
|
+
*
|
|
2124
|
+
* Bound on the editor wrapper, so it covers every step — including the value
|
|
2125
|
+
* step's real metadata widget — and runs after the widgets' own key handling.
|
|
2126
|
+
* The `event` is optional because the confirm button calls this from a click.
|
|
1903
2127
|
*/
|
|
1904
|
-
onEnter() {
|
|
2128
|
+
onEnter(event) {
|
|
2129
|
+
if (event?.defaultPrevented)
|
|
2130
|
+
return;
|
|
1905
2131
|
if (this.auto().isOpen)
|
|
1906
2132
|
return;
|
|
1907
|
-
if (this
|
|
2133
|
+
if (event && this.#isMultilineTarget(event))
|
|
1908
2134
|
return;
|
|
1909
|
-
//
|
|
1910
|
-
|
|
2135
|
+
// The inline editor owns Enter while it is open: commit when the condition is
|
|
2136
|
+
// ready, otherwise swallow the key — a half-built condition must not submit.
|
|
2137
|
+
event?.preventDefault();
|
|
1911
2138
|
const field = this.ctrl.pendingField();
|
|
1912
2139
|
const fieldOp = field?.operator ?? '';
|
|
1913
2140
|
if (!field || !fieldOp)
|
|
1914
2141
|
return;
|
|
2142
|
+
// A valueless operator (`is empty`, a date preset) is complete the moment it is
|
|
2143
|
+
// set, and the editor parks on the operator step for it — so confirm has to work
|
|
2144
|
+
// from there too, otherwise re-opening such a chip is a dead end with no way out.
|
|
2145
|
+
const onOperatorStep = this.ctrl.step() === 'operator';
|
|
2146
|
+
if (this.ctrl.step() !== 'value' && !(onOperatorStep && isValuelessOperator(fieldOp)))
|
|
2147
|
+
return;
|
|
2148
|
+
// No value guard: a blank value commits an unset placeholder (fill-in template).
|
|
2149
|
+
// The operator step has no value widget; buildCommitCondition derives the value a
|
|
2150
|
+
// valueless operator carries (the date preset id, or '') from the operator itself.
|
|
2151
|
+
const raw = onOperatorStep ? '' : this.ctrl.valueCtrl.value;
|
|
1915
2152
|
this.ctrl.commitCondition(this.ctrl.buildCommitCondition(field, fieldOp, this.ctrl.operatorLabel(fieldOp), normalizeConditionValue(raw)));
|
|
2153
|
+
// Committing unmounts the editor, which would drop focus to <body>. Hand it to
|
|
2154
|
+
// the add-type input so the next Enter (or the next condition) has a target.
|
|
2155
|
+
setTimeout(() => this.#focusTypeInput());
|
|
2156
|
+
}
|
|
2157
|
+
/**
|
|
2158
|
+
* Enter that bubbled all the way up to the component host without being claimed:
|
|
2159
|
+
* submit the search, the way a plain HTML form does.
|
|
2160
|
+
*
|
|
2161
|
+
* Everything inside that owns Enter marks the event handled via `preventDefault()`
|
|
2162
|
+
* — Material's autocomplete and select do, and so do {@link onEnter} /
|
|
2163
|
+
* {@link onTypeEnter} — so this only sees the "nothing else wanted it" case. The
|
|
2164
|
+
* remaining guards cover keys the browser itself is still acting on.
|
|
2165
|
+
*/
|
|
2166
|
+
onHostEnter(event) {
|
|
2167
|
+
if (event.defaultPrevented)
|
|
2168
|
+
return;
|
|
2169
|
+
// Confirming an IME candidate, not submitting.
|
|
2170
|
+
if (this.#isComposing(event))
|
|
2171
|
+
return;
|
|
2172
|
+
// Enter inserts a newline here; Shift+Enter never reaches us (Angular's
|
|
2173
|
+
// `keydown.enter` matches modifiers exactly).
|
|
2174
|
+
if (this.#isMultilineTarget(event))
|
|
2175
|
+
return;
|
|
2176
|
+
// A native button/link already turned this Enter into a click.
|
|
2177
|
+
if (this.#isActivationTarget(event))
|
|
2178
|
+
return;
|
|
2179
|
+
// A suggestion panel is up but Material left the key untouched (no active option).
|
|
2180
|
+
if (this.auto().isOpen)
|
|
2181
|
+
return;
|
|
2182
|
+
// A raw value renderer mounted its UI in the CDK overlay (mat-select panel,
|
|
2183
|
+
// datepicker dialog) — same reasoning as onInlineBlur.
|
|
2184
|
+
if (document.querySelector('.cdk-overlay-container .cdk-overlay-pane'))
|
|
2185
|
+
return;
|
|
2186
|
+
// The Enter that staged a type can land here after the panel already closed
|
|
2187
|
+
// (matChipInput reorders the keydown listeners — see _suppressTypeConfirm).
|
|
2188
|
+
if (this._suppressTypeConfirm || this._pickerJustSelected)
|
|
2189
|
+
return;
|
|
2190
|
+
this.#flushPendingInput();
|
|
2191
|
+
this.querySubmit.emit(this.ctrl.cmisQuery());
|
|
1916
2192
|
}
|
|
1917
2193
|
/**
|
|
1918
2194
|
* Called when the inline-input wrapper loses focus.
|
|
@@ -1986,8 +2262,13 @@ class SmartSearchComponent {
|
|
|
1986
2262
|
this.ctrl.cancelPending();
|
|
1987
2263
|
});
|
|
1988
2264
|
}
|
|
1989
|
-
/**
|
|
2265
|
+
/**
|
|
2266
|
+
* Abandon the in-progress condition edit, discarding any partial input. When an
|
|
2267
|
+
* existing condition was opened for editing it is put back verbatim rather than
|
|
2268
|
+
* lost — cancelling an edit means "leave it as it was", the same as the blur path.
|
|
2269
|
+
*/
|
|
1990
2270
|
cancelPending() {
|
|
2271
|
+
this.ctrl.restoreEditingSnapshot();
|
|
1991
2272
|
this.ctrl.cancelPending();
|
|
1992
2273
|
}
|
|
1993
2274
|
/** Jump the inline editor back to the field step so the user can re-pick the field. */
|
|
@@ -2094,8 +2375,9 @@ class SmartSearchComponent {
|
|
|
2094
2375
|
}
|
|
2095
2376
|
else {
|
|
2096
2377
|
this.ctrl.pendingField.set({ ...field, operator: item.id });
|
|
2378
|
+
// The pill reads from pendingOperatorLabel; the control is only the filter box.
|
|
2097
2379
|
this.ctrl.pendingOperatorLabel.set(item.label);
|
|
2098
|
-
this.ctrl.operatorCtrl.setValue(
|
|
2380
|
+
this.ctrl.operatorCtrl.setValue('', { emitEvent: false });
|
|
2099
2381
|
this.ctrl.inputTerm.set('');
|
|
2100
2382
|
this.ctrl.step.set('value');
|
|
2101
2383
|
this._suppressNextBlur = true;
|
|
@@ -2115,15 +2397,57 @@ class SmartSearchComponent {
|
|
|
2115
2397
|
const raw = this.ctrl.valueCtrl.value;
|
|
2116
2398
|
this.ctrl.commitCondition(this.ctrl.buildCommitCondition(field, operator, this.ctrl.operatorLabel(operator), normalizeConditionValue(raw)));
|
|
2117
2399
|
}
|
|
2400
|
+
// ── Submit-on-Enter internals (see onHostEnter) ────────────────────────────
|
|
2401
|
+
/**
|
|
2402
|
+
* Apply the debounced inputs (full-text term, fill-out form rows) to the query state
|
|
2403
|
+
* right now. Enter typically arrives well inside the debounce window, so without this
|
|
2404
|
+
* a submit would emit the query as it stood one keystroke ago. Both mutators ignore an
|
|
2405
|
+
* unchanged value, so this is free when nothing is pending.
|
|
2406
|
+
*/
|
|
2407
|
+
#flushPendingInput() {
|
|
2408
|
+
this.ctrl.setFulltextTerm(this.fulltextTermCtrl.value ?? '');
|
|
2409
|
+
if (this.ctrl.formMode())
|
|
2410
|
+
this.ctrl.flushFormValues();
|
|
2411
|
+
}
|
|
2412
|
+
/** Whether the key is confirming an IME candidate rather than acting on its own. */
|
|
2413
|
+
#isComposing(event) {
|
|
2414
|
+
const keyEvent = event;
|
|
2415
|
+
return !!keyEvent.isComposing || keyEvent.keyCode === IME_KEY_CODE;
|
|
2416
|
+
}
|
|
2417
|
+
/** Whether the key landed in a control where Enter inserts a line break. */
|
|
2418
|
+
#isMultilineTarget(event) {
|
|
2419
|
+
const target = event.target;
|
|
2420
|
+
return !!target?.closest?.('textarea, [contenteditable]:not([contenteditable="false"])');
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Whether the key landed on an element the browser activates with Enter. Native
|
|
2424
|
+
* buttons fire a click *and* let the keydown bubble, so without this an Enter on
|
|
2425
|
+
* e.g. the block-remove button would also submit the search.
|
|
2426
|
+
*/
|
|
2427
|
+
#isActivationTarget(event) {
|
|
2428
|
+
const target = event.target;
|
|
2429
|
+
return !!target?.closest?.('button, a[href], [role="button"], summary');
|
|
2430
|
+
}
|
|
2118
2431
|
#focusInput() {
|
|
2119
2432
|
this.#host.nativeElement.querySelector('.inline-input input')?.focus();
|
|
2120
2433
|
}
|
|
2434
|
+
/** Focus the add-type input — the editor's resting place once a condition is committed. */
|
|
2435
|
+
#focusTypeInput() {
|
|
2436
|
+
const input = this.#host.nativeElement.querySelector('.add-type-row input');
|
|
2437
|
+
if (!input)
|
|
2438
|
+
return;
|
|
2439
|
+
input.focus();
|
|
2440
|
+
// Focusing an autocomplete input pops its panel open. The user just finished a
|
|
2441
|
+
// condition and didn't ask for type suggestions — and an open panel would swallow
|
|
2442
|
+
// the Enter that submits the search. Typing reopens it.
|
|
2443
|
+
this.trigger()?.closePanel();
|
|
2444
|
+
}
|
|
2121
2445
|
/** Focus the "Add condition" (+) button of a block — used after confirming its types. */
|
|
2122
2446
|
#focusAddCondition(blockId) {
|
|
2123
2447
|
this.#host.nativeElement.querySelector(`.block[data-block-id="${blockId}"] .add-condition-btn`)?.focus();
|
|
2124
2448
|
}
|
|
2125
2449
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SmartSearchComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2126
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SmartSearchComponent, isStandalone: true, selector: "yuv-smart-search", inputs: { types: { classPropertyName: "types", publicName: "types", isSignal: true, isRequired: false, transformFunction: null }, skipProperties: { classPropertyName: "skipProperties", publicName: "skipProperties", isSignal: true, isRequired: false, transformFunction: null }, supportDynamicConditions: { classPropertyName: "supportDynamicConditions", publicName: "supportDynamicConditions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { queryChange: "queryChange" }, providers: [SmartSearchEditController], viewQueries: [{ propertyName: "auto", first: true, predicate: ["auto"], descendants: true, isSignal: true }, { propertyName: "trigger", first: true, predicate: MatAutocompleteTrigger, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"smart-search\" halo-container halo-container-skip=\"true\">\n <!-- Condition chip template \u2014 passed down into the recursive group component. -->\n <ng-template #chipTpl let-condition let-container=\"container\" let-i=\"index\">\n <div\n class=\"condition-chip\"\n [class.condition-chip--unset]=\"isUnset(condition)\"\n [class.condition-chip--dynamic]=\"condition.dynamic\"\n tabindex=\"0\"\n role=\"button\"\n [attr.aria-label]=\"'yuv.smart-search.condition.edit-aria' | translate: { label: condition.conditionLabel }\"\n (click)=\"editCondition(container, condition, i)\"\n (keydown.space)=\"$event.preventDefault(); editCondition(container, condition, i)\"\n (keydown.enter)=\"$event.preventDefault(); editCondition(container, condition, i)\"\n >\n @if (supportDynamicConditions() && !isValueless(condition.operator)) {\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"condition-chip__dynamic\"\n tabindex=\"-1\"\n [attr.aria-pressed]=\"!!condition.dynamic\"\n [matTooltip]=\"\n (condition.dynamic\n ? 'yuv.smart-search.condition.unmark-dynamic'\n : 'yuv.smart-search.condition.mark-dynamic'\n ) | translate\n \"\n (click)=\"$event.stopPropagation(); toggleDynamic(container, condition)\"\n >\n <mat-icon>bolt</mat-icon>\n </button>\n }\n\n <span class=\"condition-chip__part condition-chip__field\">\n <!-- <mat-icon>tune</mat-icon> -->\n {{ condition.fieldLabel }}\n </span>\n <span class=\"condition-chip__part condition-chip__op\">{{ condition.operatorLabel }}</span>\n <span class=\"condition-chip__part condition-chip__value\">\n @if (isUnset(condition)) {\n <span class=\"condition-chip__placeholder\">{{ 'yuv.smart-search.condition.unset-value' | translate }}</span>\n } @else if (valueRendererInput(condition); as rendererInput) {\n <ng-container *yuvRenderer=\"rendererInput\" />\n } @else {\n {{ condition.value }}\n }\n </span>\n <!-- @if (supportDynamicConditions() && !isValueless(condition.operator)) {\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"condition-chip__dynamic\"\n tabindex=\"-1\"\n [attr.aria-pressed]=\"!!condition.dynamic\"\n [matTooltip]=\"\n (condition.dynamic\n ? 'yuv.smart-search.condition.unmark-dynamic'\n : 'yuv.smart-search.condition.mark-dynamic'\n ) | translate\n \"\n (click)=\"$event.stopPropagation(); toggleDynamic(container, condition)\"\n >\n <mat-icon>bolt</mat-icon>\n </button>\n } -->\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"condition-chip__remove\"\n tabindex=\"-1\"\n [attr.aria-label]=\"'yuv.smart-search.condition.remove' | translate\"\n (click)=\"$event.stopPropagation(); removeCondition(container, condition)\"\n >\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </ng-template>\n\n <!-- Inline editor template \u2014 rendered inside whichever container is active. -->\n <ng-template #editorTpl>\n <div class=\"inline-input\" tabindex=\"-1\" (focusout)=\"onInlineBlur($event)\" (keydown.escape)=\"cancelPending()\">\n @if (ctrl.step() !== 'field' && ctrl.pendingField()) {\n <button\n class=\"part-pill\"\n [class.part-pill--active]=\"ctrl.step() === 'field'\"\n (click)=\"editField()\"\n [matTooltip]=\"'yuv.smart-search.field.change' | translate\"\n >\n {{ ctrl.pendingField()?.label }}\n </button>\n }\n\n @if (ctrl.step() === 'value' && ctrl.operatorCtrl.value) {\n <button\n class=\"part-pill\"\n [class.part-pill--active]=\"ctrl.step() === 'operator'\"\n (click)=\"editOperator()\"\n [matTooltip]=\"'yuv.smart-search.operator.change' | translate\"\n >\n {{ ctrl.operatorCtrl.value }}\n </button>\n }\n\n @if (ctrl.step() === 'field') {\n <input\n [formControl]=\"ctrl.fieldCtrl\"\n [matAutocomplete]=\"auto\"\n [placeholder]=\"'yuv.smart-search.field.pick' | translate\"\n (keydown.enter)=\"onEnter()\"\n />\n } @else if (ctrl.step() === 'operator') {\n <input\n [formControl]=\"ctrl.operatorCtrl\"\n [matAutocomplete]=\"auto\"\n [placeholder]=\"'yuv.smart-search.operator.select' | translate\"\n (keydown.enter)=\"onEnter()\"\n />\n } @else if (ctrl.step() === 'value') {\n @let otf = valueFieldDef();\n @if (otf) {\n <yuv-metadata-form-field variant=\"raw\" [field]=\"otf\" situation=\"EDIT\" [formControl]=\"ctrl.valueCtrl\" />\n } @else {\n <input\n [formControl]=\"ctrl.valueCtrl\"\n [matAutocomplete]=\"auto\"\n [placeholder]=\"'yuv.smart-search.value.enter' | translate\"\n (keydown.enter)=\"onEnter()\"\n />\n }\n }\n\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"confirm-btn\"\n [disabled]=\"!ctrl.isConditionComplete()\"\n (click)=\"onEnter()\"\n [matTooltip]=\"'yuv.smart-search.confirm' | translate\"\n >\n <mat-icon>check</mat-icon>\n </button>\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"cancel-btn\"\n (click)=\"cancelPending()\"\n [matTooltip]=\"'yuv.smart-search.cancel' | translate\"\n >\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </ng-template>\n\n <!-- \u2500\u2500 Toolbar: Essentials \u21C4 Full Form mode switch (only when the dynamic feature is enabled) \u2500\u2500 -->\n @if (supportDynamicConditions() && (hasDynamicConditions() || formMode())) {\n <div class=\"toolbar\">\n <div class=\"toolbar__mode\" role=\"group\" [attr.aria-label]=\"'yuv.smart-search.mode.label' | translate\">\n <button\n type=\"button\"\n class=\"toolbar__mode-btn\"\n [class.toolbar__mode-btn--active]=\"formMode()\"\n [attr.aria-pressed]=\"formMode()\"\n (click)=\"enterFormMode()\"\n >\n {{ 'yuv.smart-search.mode.essentials' | translate }}\n </button>\n <button\n type=\"button\"\n class=\"toolbar__mode-btn\"\n [class.toolbar__mode-btn--active]=\"!formMode()\"\n [attr.aria-pressed]=\"!formMode()\"\n (click)=\"exitFormMode()\"\n >\n {{ 'yuv.smart-search.mode.full' | translate }}\n </button>\n </div>\n </div>\n }\n\n <!-- \u2500\u2500 Generated fill-out form of the dynamic conditions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (formMode()) {\n <div class=\"dynamic-form\">\n @for (grp of formBlocks(); track grp.block.id; let last = $last) {\n <div class=\"dynamic-form__block\">\n <!-- Muted header listing the block's target types (a block matches any of them). -->\n <div class=\"dynamic-form__block-header\">\n @for (t of grp.block.types; track t.id) {\n @if (!$first) {\n <span class=\"dynamic-form__type-sep\">{{ 'yuv.smart-search.combinator.or' | translate }}</span>\n }\n <span class=\"dynamic-form__type\">{{ t.label }}</span>\n }\n </div>\n\n <div class=\"dynamic-form__rows\">\n @for (f of grp.fields; track f.condition) {\n <div class=\"dynamic-form__row\">\n <span class=\"dynamic-form__label\">{{ f.condition.fieldLabel }}</span>\n <span class=\"dynamic-form__op\">{{ f.condition.operatorLabel }}</span>\n <div class=\"dynamic-form__value\">\n @if (f.def) {\n <yuv-metadata-form-field variant=\"raw\" [field]=\"f.def\" situation=\"EDIT\" [formControl]=\"f.control\" />\n } @else {\n <input [formControl]=\"f.control\" [placeholder]=\"'yuv.smart-search.value.enter' | translate\" />\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Blocks are type-scoped and always combine with OR (\"this type as well as that type\"). -->\n @if (!last) {\n <div class=\"combinator\">\n <span class=\"combinator__label\">{{ 'yuv.smart-search.combinator.as-well-as' | translate }}</span>\n </div>\n }\n } @empty {\n <span class=\"dynamic-form__empty\">{{ 'yuv.smart-search.mode.empty' | translate }}</span>\n }\n </div>\n } @else {\n <!-- \u2500\u2500 Full-text search bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"fulltext\" [class.muted]=\"!fulltext().term.trim() && ctrl.blocks().length\">\n <!-- Row 1: full-width term -->\n <div class=\"fulltext__term\">\n <mat-icon class=\"fulltext__icon\">search</mat-icon>\n <input\n class=\"fulltext__input\"\n [formControl]=\"fulltextTermCtrl\"\n [placeholder]=\"'yuv.smart-search.fulltext.placeholder' | translate\"\n />\n </div>\n\n <!-- Row 2: scope (single) + types (multiple) -->\n <div class=\"fulltext__filters\">\n <mat-select\n class=\"fulltext__scope\"\n [panelWidth]=\"null\"\n panelClass=\"smart-search-select-panel\"\n [value]=\"fulltext().scope\"\n (selectionChange)=\"setFulltextScope($event.value)\"\n [aria-label]=\"'yuv.smart-search.fulltext.scope.label' | translate\"\n >\n <mat-option value=\"all\">{{ 'yuv.smart-search.fulltext.scope.all' | translate }}</mat-option>\n <mat-option value=\"metadata\">{{ 'yuv.smart-search.fulltext.scope.metadata' | translate }}</mat-option>\n <mat-option value=\"content\">{{ 'yuv.smart-search.fulltext.scope.content' | translate }}</mat-option>\n </mat-select>\n\n <mat-select\n class=\"fulltext__types\"\n multiple\n [panelWidth]=\"null\"\n panelClass=\"smart-search-select-panel\"\n [value]=\"fulltextTypeSelection()\"\n (selectionChange)=\"onFulltextTypesChange($event.value)\"\n [aria-label]=\"'yuv.smart-search.fulltext.types.label' | translate\"\n >\n <mat-option [value]=\"ALL_TYPES\">{{ 'yuv.smart-search.fulltext.types.all' | translate }}</mat-option>\n @for (t of objectTypes(); track t.id) {\n <mat-option [value]=\"t.id\">{{ t.label ?? t.id }}</mat-option>\n }\n </mat-select>\n </div>\n </div>\n\n <!-- Joiner between the full-text unit and the condition blocks. Top-level units\n always combine with OR (\"looking for this as well as that\"), so this is a\n static label rather than a toggle. -->\n @if (fulltext().term.trim() && ctrl.blocks().length) {\n <div class=\"combinator\">\n <span class=\"combinator__label\">{{ 'yuv.smart-search.combinator.as-well-as' | translate }}</span>\n </div>\n }\n\n <!-- \u2500\u2500 Step 1: Type blocks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @for (block of ctrl.blocks(); track block.id) {\n <div class=\"block\" [attr.data-block-id]=\"block.id\" [class.block--active]=\"ctrl.activeBlock() === block\">\n <div class=\"block__header\">\n <div class=\"block__types\">\n @for (t of block.types; track t.id) {\n @if (!$first) {\n <span class=\"block__type-sep\">{{ 'yuv.smart-search.combinator.or' | translate }}</span>\n }\n <span class=\"block__type\">\n <span class=\"block__type-label\">{{ t.label }}</span>\n </span>\n }\n </div>\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"block__remove\"\n (click)=\"removeBlock(block)\"\n [matTooltip]=\"'yuv.smart-search.type.remove' | translate\"\n >\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <div class=\"block__conditions\">\n <yuv-smart-search-group [group]=\"block\" [bare]=\"true\" [chipTpl]=\"chipTpl\" [editorTpl]=\"editorTpl\" />\n </div>\n </div>\n\n <!-- Static joiner between blocks \u2014 blocks are type-scoped, so they always\n combine with OR (\"this type as well as that type\"). -->\n @if (!$last && ctrl.showCombinator()) {\n <div class=\"combinator\">\n <span class=\"combinator__label\">{{ 'yuv.smart-search.combinator.as-well-as' | translate }}</span>\n </div>\n }\n }\n\n <!-- \u2500\u2500 Step 1 input: add a type block (multi-select) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (ctrl.step() === 'type') {\n <div class=\"add-type-row\">\n <button\n type=\"button\"\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"add-type-row__icon\"\n (click)=\"typeInput.focus()\"\n [attr.aria-label]=\"'yuv.smart-search.add-type' | translate\"\n >\n <mat-icon>add_circle_outline</mat-icon>\n </button>\n\n <!-- Staged draft types as removable Material chips -->\n <mat-chip-grid\n #typeChipGrid\n class=\"add-type-row__chips\"\n [attr.aria-label]=\"'yuv.smart-search.add-type' | translate\"\n >\n @for (t of ctrl.draftTypes(); track t.id) {\n <mat-chip-row (removed)=\"ctrl.removeDraftType(t.id)\">\n {{ t.label }}\n <button matChipRemove [attr.aria-label]=\"'yuv.smart-search.type.remove-draft' | translate\">\n <mat-icon>close</mat-icon>\n </button>\n </mat-chip-row>\n }\n <input\n #typeInput\n [formControl]=\"ctrl.fieldCtrl\"\n [placeholder]=\"'yuv.smart-search.add-type' | translate\"\n [matAutocomplete]=\"auto\"\n [matChipInputFor]=\"typeChipGrid\"\n (keydown.enter)=\"onTypeEnter()\"\n (keydown.escape)=\"cancelPending()\"\n />\n </mat-chip-grid>\n\n @if (ctrl.draftTypes().length) {\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"add-type-row__confirm\"\n (click)=\"confirmTypes()\"\n [matTooltip]=\"'yuv.smart-search.type.confirm' | translate\"\n >\n <mat-icon>check</mat-icon>\n </button>\n }\n </div>\n }\n }\n\n <!-- \u2500\u2500 Shared autocomplete panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <mat-autocomplete\n #auto\n panelWidth=\"auto\"\n [displayWith]=\"displayFn\"\n (optionSelected)=\"onSuggestionSelected($event)\"\n (closed)=\"onPickerClosed()\"\n >\n @for (s of ctrl.suggestions(); track s.id) {\n <mat-option [value]=\"s\">\n <div class=\"suggestion\">\n @if (s.kind !== 'type') {\n <mat-icon class=\"suggestion__icon\">\n @switch (s.kind) {\n @case ('field') {\n tune\n }\n @case ('date-preset') {\n calendar_today\n }\n @default {\n manage_search\n }\n }\n </mat-icon>\n }\n <span class=\"suggestion__label\">{{ s.label }}</span>\n </div>\n </mat-option>\n }\n </mat-autocomplete>\n</div>\n", styles: [":host{display:block;--outline: rgb(from var(--ymt-text-color) r g b / .5);--focus-visible-border-color: var(--ymt-primary);--focus-visible-border-shadow-color: rgb(from var(--ymt-primary) r g b / .3)}::ng-deep .smart-search-select-panel.mat-mdc-select-panel{min-width:max-content}::ng-deep .smart-search-select-panel.mat-mdc-select-panel .mat-mdc-option .mdc-list-item__primary-text{white-space:nowrap}.smart-search{display:flex;flex-direction:column;gap:var(--ymt-spacing-xs)}.fulltext{display:flex;flex-direction:column;gap:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);border:1px solid var(--outline);border-radius:var(--ymt-corner-s);transition:opacity .15s}.fulltext.muted{opacity:.7}.fulltext.muted:hover,.fulltext.muted:focus-within{opacity:1}.fulltext__term{display:flex;align-items:center;gap:var(--ymt-spacing-xs)}.fulltext__icon{color:var(--ymt-text-color-subtle)}.fulltext__input{flex:1;min-width:120px;border:none;outline:none;background:transparent;font:inherit;color:var(--ymt-text-color)}.fulltext__input::placeholder{color:var(--ymt-text-color-subtle);opacity:.7}.fulltext__filters{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:var(--ymt-spacing-s)}.fulltext__scope,.fulltext__types{width:auto;font-size:.9em;color:var(--ymt-text-color-subtle);border:1px solid var(--ymt-outline);border-radius:var(--ymt-corner-xs);padding-inline:var(--ymt-spacing-xs)}.fulltext__scope:focus-visible,.fulltext__types:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.block{border:1px solid var(--outline);border-radius:var(--ymt-corner-s);overflow:hidden;transition:border-color .15s;padding:var(--ymt-spacing-2xs)}.block:focus-within{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 3px var(--focus-visible-border-shadow-color)}.block__header{border-radius:var(--ymt-corner-xs);display:flex;flex-wrap:wrap;align-items:center;gap:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);background:var(--ymt-surface-container-high)}.block__types{flex:1;display:flex;align-items:center;flex-wrap:wrap}.block__type{display:inline-flex;align-items:center;gap:var(--ymt-spacing-2xs)}.block__type-sep{font:var(--ymt-font-body-subtle);text-transform:lowercase;color:var(--ymt-text-color-subtle);padding:0 var(--ymt-spacing-xs)}.block__remove{margin-inline-start:auto;opacity:.7}.block__conditions{display:flex;flex-direction:column;align-items:stretch;gap:var(--ymt-spacing-2xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);min-height:44px}.condition-chip{display:flex;flex:1;align-items:center;gap:0;border-radius:var(--ymt-corner-xs);border:1px solid var(--outline);cursor:pointer;overflow:hidden;outline:none;gap:var(--ymt-spacing-2xs)}.condition-chip:hover{border-color:var(--ymt-outline)}.condition-chip:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.condition-chip__part{display:inline-flex;align-items:center;gap:var(--ymt-spacing-4xs);padding:0 var(--ymt-spacing-xs);height:100%;white-space:nowrap;color:var(--ymt-text-color)}.condition-chip__part:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.condition-chip__op{font:var(--ymt-font-body-subtle);color:var(--ymt-text-color-subtle);padding:0}.condition-chip__value{flex:1;overflow:hidden;text-overflow:ellipsis;min-height:24px;--tile-slot-padding: 0;--yuv-renderer-display: inline-flex}.condition-chip__remove{--mat-icon-button-container-shape: 0;color:var(--ymt-text-color-subtle)}.condition-chip__remove:hover{color:var(--ymt-text-color)}.condition-chip__placeholder{font:var(--ymt-font-body-subtle);font-style:italic;color:var(--ymt-text-color-subtle);opacity:.8}.condition-chip__dynamic{--mat-icon-button-container-shape: 0;color:var(--ymt-text-color-subtle)}.condition-chip__dynamic:hover{color:var(--ymt-text-color)}.condition-chip--dynamic{outline:2px solid var(--ymt-inverse-surface);outline-offset:1px}.condition-chip--dynamic .condition-chip__dynamic{border-radius:0;background-color:var(--ymt-inverse-surface);color:var(--ymt-on-inverse-surface)}.toolbar{display:flex;justify-content:flex-end;align-items:center;background:var(--ymt-surface-container-high);border-radius:var(--ymt-corner-xs);border:1px solid var(--ymt-outline);padding:var(--ymt-spacing-3xs)}.toolbar__mode{display:inline-flex;gap:0}.toolbar__mode-btn{--bg: transparent;--fg: var(--ymt-text-color);--mdc-shape-small: var(--ymt-corner-xs);font-size:.82em;background-color:var(--bg);color:var(--fg);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-xs);border:1px solid var(--ymt-inverse-surface);cursor:pointer}.toolbar__mode-btn:focus-visible{box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.toolbar__mode-btn:first-child{border-radius:var(--mdc-shape-small) 0 0 var(--mdc-shape-small)}.toolbar__mode-btn:last-child{border-radius:0 var(--mdc-shape-small) var(--mdc-shape-small) 0}.toolbar__mode-btn--active{--bg: var(--ymt-inverse-surface);--fg: var(--ymt-on-inverse-surface)}.dynamic-form{display:flex;flex-direction:column;gap:var(--ymt-spacing-xs)}.dynamic-form__block{container-type:inline-size;border:1px solid var(--outline);border-radius:var(--ymt-corner-s);overflow:hidden}.dynamic-form__block-header{display:flex;flex-wrap:wrap;align-items:center;gap:var(--ymt-spacing-xs);color:var(--ymt-text-color-subtle);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-s);font:var(--ymt-font-body-subtle)}.dynamic-form__type-sep{text-transform:lowercase;color:var(--ymt-text-color-subtle);padding:0 var(--ymt-spacing-2xs)}.dynamic-form__rows{display:grid;grid-template-columns:max-content max-content 1fr;row-gap:var(--ymt-spacing-s);column-gap:var(--ymt-spacing-xs);overflow:auto;padding:var(--ymt-spacing-s)}.dynamic-form__row{display:grid;grid-template-columns:subgrid;grid-column:1/-1;align-items:center}.dynamic-form__label{color:var(--ymt-text-color)}.dynamic-form__op{font:var(--ymt-font-body-subtle);color:var(--ymt-text-color-subtle);border:1px solid var(--outline);padding-inline:var(--ymt-spacing-2xs);border-radius:var(--ymt-corner-xs)}.dynamic-form__value{flex:1;padding:var(--ymt-spacing-xs);border:1px solid var(--outline);border-radius:var(--ymt-corner-xs)}.dynamic-form__value input{width:100%;border:none;outline:none;background:transparent;font:inherit;color:var(--ymt-text-color)}.dynamic-form__empty{font:var(--ymt-font-body-subtle);color:var(--ymt-text-color-subtle)}@container (max-width: 499px){.dynamic-form__rows{grid-template-columns:1fr auto}.dynamic-form__row{grid-template-columns:subgrid;grid-column:1/-1;row-gap:var(--ymt-spacing-2xs)}.dynamic-form__value{grid-column:1/-1}}.inline-input{display:flex;align-items:center;gap:var(--ymt-spacing-2xs);flex:1;border:1px dashed var(--outline);border-radius:var(--ymt-corner-xs);padding:var(--ymt-spacing-2xs);background:var(--ymt-surface)}.inline-input yuv-metadata-form-field{border-radius:var(--ymt-corner-xs)}.inline-input yuv-metadata-form-field:focus-within{outline:1px solid var(--focus-visible-border-color)}.inline-input .part-pill{display:inline-flex;align-items:center;gap:var(--ymt-spacing-4xs);white-space:nowrap;font:inherit;font-size:var(--ymt-font-body-subtle-size);color:var(--ymt-text-color);background:var(--ymt-surface-container);border:1px solid var(--ymt-outline);border-radius:var(--ymt-corner-xs);padding:var(--ymt-spacing-4xs) var(--ymt-spacing-xs);cursor:pointer;transition:border-color .1s,background .1s}.inline-input .part-pill:hover{background:var(--ymt-surface-container-high)}.inline-input .part-pill:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.inline-input .part-pill--active{border-color:var(--ymt-primary);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.inline-input .part-pill mat-icon{font-size:var(--ymt-sizing-xs);width:var(--ymt-sizing-xs);height:var(--ymt-sizing-xs)}.inline-input yuv-metadata-form-field{flex:1;min-width:150px}.inline-input input{flex:1;border:none;outline:none;background:transparent}.inline-input .confirm-btn:not(:disabled){opacity:1;color:var(--ymt-color-accent)}.combinator{display:flex;justify-content:center;gap:0}.combinator__label{font:var(--ymt-font-body-subtle);text-transform:lowercase;color:var(--ymt-text-color-subtle);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-xs)}.add-type-row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);border:1px dashed var(--ymt-outline);border-radius:var(--ymt-corner-s);color:var(--ymt-text-color-subtle)}.add-type-row__icon{display:inline-flex;align-items:center;padding:0;border:0;background:transparent;color:inherit;cursor:pointer}.add-type-row__icon mat-icon{font-size:var(--ymt-sizing-m);width:var(--ymt-sizing-m);height:var(--ymt-sizing-m);opacity:.5}.add-type-row__icon:hover mat-icon{opacity:.8}.add-type-row__confirm{color:var(--ymt-primary)}.add-type-row__chips{flex:1;min-width:120px}.add-type-row input{flex:1;min-width:120px;border:none;outline:none;background:transparent;font:inherit;font-size:.9em;color:inherit}.add-type-row input::placeholder{color:var(--ymt-text-color-subtle);opacity:.7}.condition-combinator{display:flex;align-self:center}.suggestion{display:flex;align-items:center;gap:var(--ymt-spacing-xs)}.suggestion__icon{font-size:var(--ymt-sizing-s);width:var(--ymt-sizing-s);height:var(--ymt-sizing-s);--icon-size: var(--ymt-sizing-s);opacity:.6}.suggestion__label{flex:1}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatAutocompleteModule }, { kind: "component", type: i2$1.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "component", type: i2$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: i2$1.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "ngmodule", type: MatChipsModule }, { kind: "component", type: i4.MatChipGrid, selector: "mat-chip-grid", inputs: ["disabled", "placeholder", "required", "value", "errorStateMatcher"], outputs: ["change", "valueChange"] }, { kind: "directive", type: i4.MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputFor", "matChipInputAddOnBlur", "matChipInputSeparatorKeyCodes", "placeholder", "id", "disabled", "readonly", "matChipInputDisabledInteractive"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }, { kind: "directive", type: i4.MatChipRemove, selector: "[matChipRemove]" }, { kind: "component", type: i4.MatChipRow, selector: "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", inputs: ["editable"], outputs: ["edited"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "directive", type: YmtIconButtonDirective, selector: "button[ymtIconButton],button[ymt-icon-button],a[ymtIconButton],a[ymt-icon-button]", inputs: ["disabled", "disableRipple", "aria-disabled", "disabledInteractive", "icon-button-size"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: MetadataFormFieldComponent, selector: "yuv-metadata-form-field", inputs: ["formChangedSubject", "field", "variant", "situation"] }, { kind: "directive", type: RendererDirective, selector: "[yuvRenderer]", inputs: ["yuvRenderer"] }, { kind: "component", type: SmartSearchGroupComponent, selector: "yuv-smart-search-group", inputs: ["group", "bare", "chipTpl", "editorTpl"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2450
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SmartSearchComponent, isStandalone: true, selector: "yuv-smart-search", inputs: { types: { classPropertyName: "types", publicName: "types", isSignal: true, isRequired: false, transformFunction: null }, skipProperties: { classPropertyName: "skipProperties", publicName: "skipProperties", isSignal: true, isRequired: false, transformFunction: null }, fulltextOnly: { classPropertyName: "fulltextOnly", publicName: "fulltextOnly", isSignal: true, isRequired: false, transformFunction: null }, supportDynamicConditions: { classPropertyName: "supportDynamicConditions", publicName: "supportDynamicConditions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { queryChange: "queryChange", querySubmit: "querySubmit" }, host: { listeners: { "keydown.enter": "onHostEnter($event)" } }, providers: [SmartSearchEditController], viewQueries: [{ propertyName: "auto", first: true, predicate: ["auto"], descendants: true, isSignal: true }, { propertyName: "trigger", first: true, predicate: MatAutocompleteTrigger, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"smart-search\" halo-container halo-container-skip=\"true\">\n <!-- Condition chip template \u2014 passed down into the recursive group component. -->\n <ng-template #chipTpl let-condition let-container=\"container\" let-i=\"index\">\n <div\n class=\"condition-chip\"\n [class.condition-chip--unset]=\"isUnset(condition)\"\n [class.condition-chip--dynamic]=\"condition.dynamic\"\n tabindex=\"0\"\n role=\"button\"\n [attr.aria-label]=\"'yuv.smart-search.condition.edit-aria' | translate: { label: condition.conditionLabel }\"\n (click)=\"editCondition(container, condition, i)\"\n (keydown.space)=\"$event.preventDefault(); editCondition(container, condition, i)\"\n (keydown.enter)=\"$event.preventDefault(); editCondition(container, condition, i)\"\n >\n @if (supportDynamicConditions() && !isValueless(condition.operator)) {\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"condition-chip__dynamic\"\n tabindex=\"-1\"\n [attr.aria-pressed]=\"!!condition.dynamic\"\n [matTooltip]=\"\n (condition.dynamic\n ? 'yuv.smart-search.condition.unmark-dynamic'\n : 'yuv.smart-search.condition.mark-dynamic'\n ) | translate\n \"\n (click)=\"$event.stopPropagation(); toggleDynamic(container, condition)\"\n >\n <mat-icon>bolt</mat-icon>\n </button>\n }\n\n <span class=\"condition-chip__part condition-chip__field\">\n <!-- <mat-icon>tune</mat-icon> -->\n {{ condition.fieldLabel }}\n </span>\n <span class=\"condition-chip__part condition-chip__op\">{{ condition.operatorLabel }}</span>\n <!-- A valueless operator states the whole condition on its own (`is empty`, or a date\n preset whose label already reads \u201CThis year\u201D), so it gets no value segment: the\n stored value is an internal marker (`thisYear`), not something to show the user. -->\n @if (!isValueless(condition.operator)) {\n <span class=\"condition-chip__part condition-chip__value\">\n @if (isUnset(condition)) {\n <span class=\"condition-chip__placeholder\">{{ 'yuv.smart-search.condition.unset-value' | translate }}</span>\n } @else if (valueRendererInput(condition); as rendererInput) {\n <ng-container *yuvRenderer=\"rendererInput\" />\n } @else {\n {{ condition.value }}\n }\n </span>\n }\n <!-- @if (supportDynamicConditions() && !isValueless(condition.operator)) {\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"condition-chip__dynamic\"\n tabindex=\"-1\"\n [attr.aria-pressed]=\"!!condition.dynamic\"\n [matTooltip]=\"\n (condition.dynamic\n ? 'yuv.smart-search.condition.unmark-dynamic'\n : 'yuv.smart-search.condition.mark-dynamic'\n ) | translate\n \"\n (click)=\"$event.stopPropagation(); toggleDynamic(container, condition)\"\n >\n <mat-icon>bolt</mat-icon>\n </button>\n } -->\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"condition-chip__remove\"\n tabindex=\"-1\"\n [attr.aria-label]=\"'yuv.smart-search.condition.remove' | translate\"\n (click)=\"$event.stopPropagation(); removeCondition(container, condition)\"\n >\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </ng-template>\n\n <!-- Inline editor template \u2014 rendered inside whichever container is active. -->\n <ng-template #editorTpl>\n <!-- Enter is bound on the wrapper, not per input, so it also covers the value step's\n real metadata widget and runs after each widget's own key handling. -->\n <div\n class=\"inline-input\"\n tabindex=\"-1\"\n (focusout)=\"onInlineBlur($event)\"\n (keydown.enter)=\"onEnter($event)\"\n (keydown.escape)=\"cancelPending()\"\n >\n @if (ctrl.step() !== 'field' && ctrl.pendingField()) {\n <button\n class=\"part-pill\"\n [class.part-pill--active]=\"ctrl.step() === 'field'\"\n (click)=\"editField()\"\n [matTooltip]=\"'yuv.smart-search.field.change' | translate\"\n >\n {{ ctrl.pendingField()?.label }}\n </button>\n }\n\n <!-- The operator is always a pill, never editable text: at the value step it is the\n way back to the operator picker, and at the operator step it shows what is\n currently set while the (empty) input below filters the replacement list. -->\n @if (ctrl.step() !== 'field' && ctrl.pendingOperatorLabel()) {\n <button\n class=\"part-pill\"\n [class.part-pill--active]=\"ctrl.step() === 'operator'\"\n (click)=\"editOperator()\"\n [matTooltip]=\"'yuv.smart-search.operator.change' | translate\"\n >\n {{ ctrl.pendingOperatorLabel() }}\n </button>\n }\n\n @if (ctrl.step() === 'field') {\n <input\n [formControl]=\"ctrl.fieldCtrl\"\n [matAutocomplete]=\"auto\"\n [placeholder]=\"'yuv.smart-search.field.pick' | translate\"\n />\n } @else if (ctrl.step() === 'operator') {\n <input\n [formControl]=\"ctrl.operatorCtrl\"\n [matAutocomplete]=\"auto\"\n [placeholder]=\"'yuv.smart-search.operator.select' | translate\"\n />\n } @else if (ctrl.step() === 'value') {\n @let otf = valueFieldDef();\n @if (otf) {\n <yuv-metadata-form-field variant=\"raw\" [field]=\"otf\" situation=\"EDIT\" [formControl]=\"ctrl.valueCtrl\" />\n } @else {\n <input\n [formControl]=\"ctrl.valueCtrl\"\n [matAutocomplete]=\"auto\"\n [placeholder]=\"'yuv.smart-search.value.enter' | translate\"\n />\n }\n }\n\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"confirm-btn\"\n [disabled]=\"!ctrl.isConditionComplete()\"\n (click)=\"onEnter()\"\n [matTooltip]=\"'yuv.smart-search.confirm' | translate\"\n >\n <mat-icon>check</mat-icon>\n </button>\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"cancel-btn\"\n (click)=\"cancelPending()\"\n [matTooltip]=\"'yuv.smart-search.cancel' | translate\"\n >\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </ng-template>\n\n <!-- \u2500\u2500 Toolbar: Essentials \u21C4 Full Form mode switch (only when the dynamic feature is enabled) \u2500\u2500 -->\n @if (!fulltextOnly() && supportDynamicConditions() && (hasDynamicConditions() || formMode())) {\n <div class=\"toolbar\">\n <div class=\"toolbar__mode\" role=\"group\" [attr.aria-label]=\"'yuv.smart-search.mode.label' | translate\">\n <button\n type=\"button\"\n class=\"toolbar__mode-btn\"\n [class.toolbar__mode-btn--active]=\"formMode()\"\n [attr.aria-pressed]=\"formMode()\"\n (click)=\"enterFormMode()\"\n >\n {{ 'yuv.smart-search.mode.essentials' | translate }}\n </button>\n <button\n type=\"button\"\n class=\"toolbar__mode-btn\"\n [class.toolbar__mode-btn--active]=\"!formMode()\"\n [attr.aria-pressed]=\"!formMode()\"\n (click)=\"exitFormMode()\"\n >\n {{ 'yuv.smart-search.mode.full' | translate }}\n </button>\n </div>\n </div>\n }\n\n <!-- \u2500\u2500 Generated fill-out form of the dynamic conditions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (formMode()) {\n <div class=\"dynamic-form\">\n @for (grp of formBlocks(); track grp.block.id; let last = $last) {\n <div class=\"dynamic-form__block\">\n <!-- Muted header listing the block's target types (a block matches any of them). -->\n <div class=\"dynamic-form__block-header\">\n @for (t of grp.block.types; track t.id) {\n @if (!$first) {\n <span class=\"dynamic-form__type-sep\">{{ 'yuv.smart-search.combinator.or' | translate }}</span>\n }\n <span class=\"dynamic-form__type\">{{ t.label }}</span>\n }\n </div>\n\n <div class=\"dynamic-form__rows\">\n @for (f of grp.fields; track f.condition) {\n <div class=\"dynamic-form__row\">\n <span class=\"dynamic-form__label\">{{ f.condition.fieldLabel }}</span>\n <span class=\"dynamic-form__op\">{{ f.condition.operatorLabel }}</span>\n <div class=\"dynamic-form__value\">\n @if (f.def) {\n <yuv-metadata-form-field variant=\"raw\" [field]=\"f.def\" situation=\"EDIT\" [formControl]=\"f.control\" />\n } @else {\n <input [formControl]=\"f.control\" [placeholder]=\"'yuv.smart-search.value.enter' | translate\" />\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Blocks are type-scoped and always combine with OR (\"this type as well as that type\"). -->\n @if (!last) {\n <div class=\"combinator\">\n <span class=\"combinator__label\">{{ 'yuv.smart-search.combinator.as-well-as' | translate }}</span>\n </div>\n }\n } @empty {\n <span class=\"dynamic-form__empty\">{{ 'yuv.smart-search.mode.empty' | translate }}</span>\n }\n </div>\n } @else {\n <!-- \u2500\u2500 Full-text search bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Muting hints \"the blocks carry the search, not me\" \u2014 with the blocks hidden there\n is nothing to defer to, so the only visible control never greys out. -->\n <div class=\"fulltext\" [class.muted]=\"!fulltextOnly() && !fulltext().term.trim() && ctrl.blocks().length\">\n <!-- Row 1: full-width term -->\n <div class=\"fulltext__term\">\n <mat-icon class=\"fulltext__icon\">search</mat-icon>\n <input\n class=\"fulltext__input\"\n [formControl]=\"fulltextTermCtrl\"\n [placeholder]=\"'yuv.smart-search.fulltext.placeholder' | translate\"\n />\n </div>\n\n <!-- Row 2: scope (single) + types (multiple) -->\n <div class=\"fulltext__filters\">\n <mat-select\n class=\"fulltext__scope\"\n [panelWidth]=\"null\"\n panelClass=\"smart-search-select-panel\"\n [value]=\"fulltext().scope\"\n (selectionChange)=\"setFulltextScope($event.value)\"\n [aria-label]=\"'yuv.smart-search.fulltext.scope.label' | translate\"\n >\n <mat-option value=\"all\">{{ 'yuv.smart-search.fulltext.scope.all' | translate }}</mat-option>\n <mat-option value=\"metadata\">{{ 'yuv.smart-search.fulltext.scope.metadata' | translate }}</mat-option>\n <mat-option value=\"content\">{{ 'yuv.smart-search.fulltext.scope.content' | translate }}</mat-option>\n </mat-select>\n\n <mat-select\n class=\"fulltext__types\"\n multiple\n [panelWidth]=\"null\"\n panelClass=\"smart-search-select-panel\"\n [value]=\"fulltextTypeSelection()\"\n (selectionChange)=\"onFulltextTypesChange($event.value)\"\n [aria-label]=\"'yuv.smart-search.fulltext.types.label' | translate\"\n >\n <mat-option [value]=\"ALL_TYPES\">{{ 'yuv.smart-search.fulltext.types.all' | translate }}</mat-option>\n @for (t of objectTypes(); track t.id) {\n <mat-option [value]=\"t.id\">{{ t.label ?? t.id }}</mat-option>\n }\n </mat-select>\n </div>\n </div>\n\n <!-- \u2500\u2500 Condition builder (hidden in full-text-only mode) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (!fulltextOnly()) {\n <!-- Joiner between the full-text unit and the condition blocks. Top-level units\n always combine with OR (\"looking for this as well as that\"), so this is a\n static label rather than a toggle. -->\n @if (fulltext().term.trim() && ctrl.blocks().length) {\n <div class=\"combinator\">\n <span class=\"combinator__label\">{{ 'yuv.smart-search.combinator.as-well-as' | translate }}</span>\n </div>\n }\n\n <!-- \u2500\u2500 Step 1: Type blocks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @for (block of ctrl.blocks(); track block.id) {\n <div class=\"block\" [attr.data-block-id]=\"block.id\" [class.block--active]=\"ctrl.activeBlock() === block\">\n <div class=\"block__header\">\n <div class=\"block__types\">\n @for (t of block.types; track t.id) {\n @if (!$first) {\n <span class=\"block__type-sep\">{{ 'yuv.smart-search.combinator.or' | translate }}</span>\n }\n <span class=\"block__type\">\n <span class=\"block__type-label\">{{ t.label }}</span>\n </span>\n }\n </div>\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"block__remove\"\n (click)=\"removeBlock(block)\"\n [matTooltip]=\"'yuv.smart-search.type.remove' | translate\"\n >\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <div class=\"block__conditions\">\n <yuv-smart-search-group [group]=\"block\" [bare]=\"true\" [chipTpl]=\"chipTpl\" [editorTpl]=\"editorTpl\" />\n </div>\n </div>\n\n <!-- Static joiner between blocks \u2014 blocks are type-scoped, so they always\n combine with OR (\"this type as well as that type\"). -->\n @if (!$last && ctrl.showCombinator()) {\n <div class=\"combinator\">\n <span class=\"combinator__label\">{{ 'yuv.smart-search.combinator.as-well-as' | translate }}</span>\n </div>\n }\n }\n\n <!-- \u2500\u2500 Step 1 input: add a type block (multi-select) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (ctrl.step() === 'type') {\n <div class=\"add-type-row\">\n <button\n type=\"button\"\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"add-type-row__icon\"\n (click)=\"typeInput.focus()\"\n [attr.aria-label]=\"'yuv.smart-search.add-type' | translate\"\n >\n <mat-icon>add_circle_outline</mat-icon>\n </button>\n\n <!-- Staged draft types as removable Material chips -->\n <mat-chip-grid\n #typeChipGrid\n class=\"add-type-row__chips\"\n [attr.aria-label]=\"'yuv.smart-search.add-type' | translate\"\n >\n @for (t of ctrl.draftTypes(); track t.id) {\n <mat-chip-row (removed)=\"ctrl.removeDraftType(t.id)\">\n {{ t.label }}\n <button matChipRemove [attr.aria-label]=\"'yuv.smart-search.type.remove-draft' | translate\">\n <mat-icon>close</mat-icon>\n </button>\n </mat-chip-row>\n }\n <input\n #typeInput\n [formControl]=\"ctrl.fieldCtrl\"\n [placeholder]=\"'yuv.smart-search.add-type' | translate\"\n [matAutocomplete]=\"auto\"\n [matChipInputFor]=\"typeChipGrid\"\n [matChipInputSeparatorKeyCodes]=\"NO_SEPARATOR_KEYS\"\n (keydown.enter)=\"onTypeEnter($event)\"\n (keydown.escape)=\"cancelPending()\"\n />\n </mat-chip-grid>\n\n @if (ctrl.draftTypes().length) {\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"add-type-row__confirm\"\n (click)=\"confirmTypes()\"\n [matTooltip]=\"'yuv.smart-search.type.confirm' | translate\"\n >\n <mat-icon>check</mat-icon>\n </button>\n }\n </div>\n }\n }\n }\n\n <!-- \u2500\u2500 Shared autocomplete panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <mat-autocomplete\n #auto\n panelWidth=\"auto\"\n [displayWith]=\"displayFn\"\n (optionSelected)=\"onSuggestionSelected($event)\"\n (closed)=\"onPickerClosed()\"\n >\n @for (s of ctrl.suggestions(); track s.id) {\n <mat-option [value]=\"s\">\n <div class=\"suggestion\">\n @if (s.kind !== 'type') {\n <mat-icon class=\"suggestion__icon\">\n @switch (s.kind) {\n @case ('field') {\n tune\n }\n @case ('date-preset') {\n calendar_today\n }\n @default {\n manage_search\n }\n }\n </mat-icon>\n }\n <span class=\"suggestion__label\">{{ s.label }}</span>\n </div>\n </mat-option>\n }\n </mat-autocomplete>\n</div>\n", styles: [":host{display:block;--outline: rgb(from var(--ymt-text-color) r g b / .5);--focus-visible-border-color: var(--ymt-primary);--focus-visible-border-shadow-color: rgb(from var(--ymt-primary) r g b / .3)}::ng-deep .smart-search-select-panel.mat-mdc-select-panel{min-width:max-content}::ng-deep .smart-search-select-panel.mat-mdc-select-panel .mat-mdc-option .mdc-list-item__primary-text{white-space:nowrap}.smart-search{display:flex;flex-direction:column;gap:var(--ymt-spacing-xs)}.fulltext{display:flex;flex-direction:column;gap:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);border:1px solid var(--outline);border-radius:var(--ymt-corner-s);transition:opacity .15s}.fulltext.muted{opacity:.7}.fulltext.muted:hover,.fulltext.muted:focus-within{opacity:1}.fulltext__term{display:flex;align-items:center;gap:var(--ymt-spacing-xs)}.fulltext__icon{color:var(--ymt-text-color-subtle)}.fulltext__input{flex:1;min-width:120px;border:none;outline:none;background:transparent;font:inherit;color:var(--ymt-text-color)}.fulltext__input::placeholder{color:var(--ymt-text-color-subtle);opacity:.7}.fulltext__filters{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:var(--ymt-spacing-s)}.fulltext__scope,.fulltext__types{width:auto;font-size:.9em;color:var(--ymt-text-color-subtle);border:1px solid var(--ymt-outline);border-radius:var(--ymt-corner-xs);padding-inline:var(--ymt-spacing-xs)}.fulltext__scope:focus-visible,.fulltext__types:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.block{border:1px solid var(--outline);border-radius:var(--ymt-corner-s);overflow:hidden;transition:border-color .15s;padding:var(--ymt-spacing-2xs)}.block:focus-within{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 3px var(--focus-visible-border-shadow-color)}.block__header{border-radius:var(--ymt-corner-xs);display:flex;flex-wrap:wrap;align-items:center;gap:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);background:var(--ymt-surface-container-high)}.block__types{flex:1;display:flex;align-items:center;flex-wrap:wrap}.block__type{display:inline-flex;align-items:center;gap:var(--ymt-spacing-2xs)}.block__type-sep{font:var(--ymt-font-body-subtle);text-transform:lowercase;color:var(--ymt-text-color-subtle);padding:0 var(--ymt-spacing-xs)}.block__remove{margin-inline-start:auto;opacity:.7}.block__conditions{display:flex;flex-direction:column;align-items:stretch;gap:var(--ymt-spacing-2xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);min-height:44px}.condition-chip{display:flex;flex:1;align-items:center;gap:0;border-radius:var(--ymt-corner-xs);border:1px solid var(--outline);cursor:pointer;overflow:hidden;outline:none;gap:var(--ymt-spacing-2xs)}.condition-chip:hover{border-color:var(--ymt-outline)}.condition-chip:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.condition-chip__part{display:inline-flex;align-items:center;gap:var(--ymt-spacing-4xs);padding:0 var(--ymt-spacing-xs);height:100%;white-space:nowrap;color:var(--ymt-text-color)}.condition-chip__part:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.condition-chip__op{font:var(--ymt-font-body-subtle);color:var(--ymt-text-color-subtle);padding:0}.condition-chip__value{flex:1;overflow:hidden;text-overflow:ellipsis;min-height:24px;--tile-slot-padding: 0;--yuv-renderer-display: inline-flex}.condition-chip__remove{--mat-icon-button-container-shape: 0;color:var(--ymt-text-color-subtle)}.condition-chip__remove:hover{color:var(--ymt-text-color)}.condition-chip__placeholder{font:var(--ymt-font-body-subtle);font-style:italic;color:var(--ymt-text-color-subtle);opacity:.8}.condition-chip__dynamic{--mat-icon-button-container-shape: 0;color:var(--ymt-text-color-subtle)}.condition-chip__dynamic:hover{color:var(--ymt-text-color)}.condition-chip--dynamic{outline:2px solid var(--ymt-inverse-surface);outline-offset:1px}.condition-chip--dynamic .condition-chip__dynamic{border-radius:0;background-color:var(--ymt-inverse-surface);color:var(--ymt-on-inverse-surface)}.toolbar{display:flex;justify-content:flex-end;align-items:center;background:var(--ymt-surface-container-high);border-radius:var(--ymt-corner-xs);border:1px solid var(--ymt-outline);padding:var(--ymt-spacing-3xs)}.toolbar__mode{display:inline-flex;gap:0}.toolbar__mode-btn{--bg: transparent;--fg: var(--ymt-text-color);--mdc-shape-small: var(--ymt-corner-xs);font-size:.82em;background-color:var(--bg);color:var(--fg);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-xs);border:1px solid var(--ymt-inverse-surface);cursor:pointer}.toolbar__mode-btn:focus-visible{box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.toolbar__mode-btn:first-child{border-radius:var(--mdc-shape-small) 0 0 var(--mdc-shape-small)}.toolbar__mode-btn:last-child{border-radius:0 var(--mdc-shape-small) var(--mdc-shape-small) 0}.toolbar__mode-btn--active{--bg: var(--ymt-inverse-surface);--fg: var(--ymt-on-inverse-surface)}.dynamic-form{display:flex;flex-direction:column;gap:var(--ymt-spacing-xs)}.dynamic-form__block{container-type:inline-size;border:1px solid var(--outline);border-radius:var(--ymt-corner-s);overflow:hidden}.dynamic-form__block-header{display:flex;flex-wrap:wrap;align-items:center;gap:var(--ymt-spacing-xs);color:var(--ymt-text-color-subtle);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-s);font:var(--ymt-font-body-subtle)}.dynamic-form__type-sep{text-transform:lowercase;color:var(--ymt-text-color-subtle);padding:0 var(--ymt-spacing-2xs)}.dynamic-form__rows{display:grid;grid-template-columns:max-content max-content 1fr;row-gap:var(--ymt-spacing-s);column-gap:var(--ymt-spacing-xs);overflow:auto;padding:var(--ymt-spacing-s)}.dynamic-form__row{display:grid;grid-template-columns:subgrid;grid-column:1/-1;align-items:center}.dynamic-form__label{color:var(--ymt-text-color)}.dynamic-form__op{font:var(--ymt-font-body-subtle);color:var(--ymt-text-color-subtle);border:1px solid var(--outline);padding-inline:var(--ymt-spacing-2xs);border-radius:var(--ymt-corner-xs)}.dynamic-form__value{flex:1;padding:var(--ymt-spacing-xs);border:1px solid var(--outline);border-radius:var(--ymt-corner-xs)}.dynamic-form__value input{width:100%;border:none;outline:none;background:transparent;font:inherit;color:var(--ymt-text-color)}.dynamic-form__empty{font:var(--ymt-font-body-subtle);color:var(--ymt-text-color-subtle)}@container (max-width: 499px){.dynamic-form__rows{grid-template-columns:1fr auto}.dynamic-form__row{grid-template-columns:subgrid;grid-column:1/-1;row-gap:var(--ymt-spacing-2xs)}.dynamic-form__value{grid-column:1/-1}}.inline-input{display:flex;align-items:center;gap:var(--ymt-spacing-2xs);flex:1;border:1px dashed var(--outline);border-radius:var(--ymt-corner-xs);padding:var(--ymt-spacing-2xs);background:var(--ymt-surface)}.inline-input yuv-metadata-form-field{border-radius:var(--ymt-corner-xs)}.inline-input yuv-metadata-form-field:focus-within{outline:1px solid var(--focus-visible-border-color)}.inline-input .part-pill{display:inline-flex;align-items:center;gap:var(--ymt-spacing-4xs);white-space:nowrap;font:inherit;font-size:var(--ymt-font-body-subtle-size);color:var(--ymt-text-color);background:var(--ymt-surface-container);border:1px solid var(--ymt-outline);border-radius:var(--ymt-corner-xs);padding:var(--ymt-spacing-4xs) var(--ymt-spacing-xs);cursor:pointer;transition:border-color .1s,background .1s}.inline-input .part-pill:hover{background:var(--ymt-surface-container-high)}.inline-input .part-pill:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.inline-input .part-pill--active{border-color:var(--ymt-primary);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.inline-input .part-pill mat-icon{font-size:var(--ymt-sizing-xs);width:var(--ymt-sizing-xs);height:var(--ymt-sizing-xs)}.inline-input yuv-metadata-form-field{flex:1;min-width:150px}.inline-input input{flex:1;border:none;outline:none;background:transparent}.inline-input .confirm-btn:not(:disabled){opacity:1;color:var(--ymt-color-accent)}.combinator{display:flex;justify-content:center;gap:0}.combinator__label{font:var(--ymt-font-body-subtle);text-transform:lowercase;color:var(--ymt-text-color-subtle);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-xs)}.add-type-row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);border:1px dashed var(--ymt-outline);border-radius:var(--ymt-corner-s);color:var(--ymt-text-color-subtle)}.add-type-row__icon{display:inline-flex;align-items:center;padding:0;border:0;background:transparent;color:inherit;cursor:pointer}.add-type-row__icon mat-icon{font-size:var(--ymt-sizing-m);width:var(--ymt-sizing-m);height:var(--ymt-sizing-m);opacity:.5}.add-type-row__icon:hover mat-icon{opacity:.8}.add-type-row__confirm{color:var(--ymt-primary)}.add-type-row__chips{flex:1;min-width:120px}.add-type-row input{flex:1;min-width:120px;border:none;outline:none;background:transparent;font:inherit;font-size:.9em;color:inherit}.add-type-row input::placeholder{color:var(--ymt-text-color-subtle);opacity:.7}.condition-combinator{display:flex;align-self:center}.suggestion{display:flex;align-items:center;gap:var(--ymt-spacing-xs)}.suggestion__icon{font-size:var(--ymt-sizing-s);width:var(--ymt-sizing-s);height:var(--ymt-sizing-s);--icon-size: var(--ymt-sizing-s);opacity:.6}.suggestion__label{flex:1}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatAutocompleteModule }, { kind: "component", type: i2$1.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "component", type: i2$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: i2$1.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "ngmodule", type: MatChipsModule }, { kind: "component", type: i4.MatChipGrid, selector: "mat-chip-grid", inputs: ["disabled", "placeholder", "required", "value", "errorStateMatcher"], outputs: ["change", "valueChange"] }, { kind: "directive", type: i4.MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputFor", "matChipInputAddOnBlur", "matChipInputSeparatorKeyCodes", "placeholder", "id", "disabled", "readonly", "matChipInputDisabledInteractive"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }, { kind: "directive", type: i4.MatChipRemove, selector: "[matChipRemove]" }, { kind: "component", type: i4.MatChipRow, selector: "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", inputs: ["editable"], outputs: ["edited"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "directive", type: YmtIconButtonDirective, selector: "button[ymtIconButton],button[ymt-icon-button],a[ymtIconButton],a[ymt-icon-button]", inputs: ["disabled", "disableRipple", "aria-disabled", "disabledInteractive", "icon-button-size"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: MetadataFormFieldComponent, selector: "yuv-metadata-form-field", inputs: ["formChangedSubject", "field", "variant", "situation"] }, { kind: "directive", type: RendererDirective, selector: "[yuvRenderer]", inputs: ["yuvRenderer"] }, { kind: "component", type: SmartSearchGroupComponent, selector: "yuv-smart-search-group", inputs: ["group", "bare", "chipTpl", "editorTpl"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2127
2451
|
}
|
|
2128
2452
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SmartSearchComponent, decorators: [{
|
|
2129
2453
|
type: Component,
|
|
@@ -2141,8 +2465,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
2141
2465
|
RendererDirective,
|
|
2142
2466
|
SmartSearchGroupComponent,
|
|
2143
2467
|
TranslatePipe
|
|
2144
|
-
], providers: [SmartSearchEditController], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"smart-search\" halo-container halo-container-skip=\"true\">\n <!-- Condition chip template \u2014 passed down into the recursive group component. -->\n <ng-template #chipTpl let-condition let-container=\"container\" let-i=\"index\">\n <div\n class=\"condition-chip\"\n [class.condition-chip--unset]=\"isUnset(condition)\"\n [class.condition-chip--dynamic]=\"condition.dynamic\"\n tabindex=\"0\"\n role=\"button\"\n [attr.aria-label]=\"'yuv.smart-search.condition.edit-aria' | translate: { label: condition.conditionLabel }\"\n (click)=\"editCondition(container, condition, i)\"\n (keydown.space)=\"$event.preventDefault(); editCondition(container, condition, i)\"\n (keydown.enter)=\"$event.preventDefault(); editCondition(container, condition, i)\"\n >\n @if (supportDynamicConditions() && !isValueless(condition.operator)) {\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"condition-chip__dynamic\"\n tabindex=\"-1\"\n [attr.aria-pressed]=\"!!condition.dynamic\"\n [matTooltip]=\"\n (condition.dynamic\n ? 'yuv.smart-search.condition.unmark-dynamic'\n : 'yuv.smart-search.condition.mark-dynamic'\n ) | translate\n \"\n (click)=\"$event.stopPropagation(); toggleDynamic(container, condition)\"\n >\n <mat-icon>bolt</mat-icon>\n </button>\n }\n\n <span class=\"condition-chip__part condition-chip__field\">\n <!-- <mat-icon>tune</mat-icon> -->\n {{ condition.fieldLabel }}\n </span>\n <span class=\"condition-chip__part condition-chip__op\">{{ condition.operatorLabel }}</span>\n <span class=\"condition-chip__part condition-chip__value\">\n @if (isUnset(condition)) {\n <span class=\"condition-chip__placeholder\">{{ 'yuv.smart-search.condition.unset-value' | translate }}</span>\n } @else if (valueRendererInput(condition); as rendererInput) {\n <ng-container *yuvRenderer=\"rendererInput\" />\n } @else {\n {{ condition.value }}\n }\n </span>\n <!-- @if (supportDynamicConditions() && !isValueless(condition.operator)) {\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"condition-chip__dynamic\"\n tabindex=\"-1\"\n [attr.aria-pressed]=\"!!condition.dynamic\"\n [matTooltip]=\"\n (condition.dynamic\n ? 'yuv.smart-search.condition.unmark-dynamic'\n : 'yuv.smart-search.condition.mark-dynamic'\n ) | translate\n \"\n (click)=\"$event.stopPropagation(); toggleDynamic(container, condition)\"\n >\n <mat-icon>bolt</mat-icon>\n </button>\n } -->\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"condition-chip__remove\"\n tabindex=\"-1\"\n [attr.aria-label]=\"'yuv.smart-search.condition.remove' | translate\"\n (click)=\"$event.stopPropagation(); removeCondition(container, condition)\"\n >\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </ng-template>\n\n <!-- Inline editor template \u2014 rendered inside whichever container is active. -->\n <ng-template #editorTpl>\n <div class=\"inline-input\" tabindex=\"-1\" (focusout)=\"onInlineBlur($event)\" (keydown.escape)=\"cancelPending()\">\n @if (ctrl.step() !== 'field' && ctrl.pendingField()) {\n <button\n class=\"part-pill\"\n [class.part-pill--active]=\"ctrl.step() === 'field'\"\n (click)=\"editField()\"\n [matTooltip]=\"'yuv.smart-search.field.change' | translate\"\n >\n {{ ctrl.pendingField()?.label }}\n </button>\n }\n\n @if (ctrl.step() === 'value' && ctrl.operatorCtrl.value) {\n <button\n class=\"part-pill\"\n [class.part-pill--active]=\"ctrl.step() === 'operator'\"\n (click)=\"editOperator()\"\n [matTooltip]=\"'yuv.smart-search.operator.change' | translate\"\n >\n {{ ctrl.operatorCtrl.value }}\n </button>\n }\n\n @if (ctrl.step() === 'field') {\n <input\n [formControl]=\"ctrl.fieldCtrl\"\n [matAutocomplete]=\"auto\"\n [placeholder]=\"'yuv.smart-search.field.pick' | translate\"\n (keydown.enter)=\"onEnter()\"\n />\n } @else if (ctrl.step() === 'operator') {\n <input\n [formControl]=\"ctrl.operatorCtrl\"\n [matAutocomplete]=\"auto\"\n [placeholder]=\"'yuv.smart-search.operator.select' | translate\"\n (keydown.enter)=\"onEnter()\"\n />\n } @else if (ctrl.step() === 'value') {\n @let otf = valueFieldDef();\n @if (otf) {\n <yuv-metadata-form-field variant=\"raw\" [field]=\"otf\" situation=\"EDIT\" [formControl]=\"ctrl.valueCtrl\" />\n } @else {\n <input\n [formControl]=\"ctrl.valueCtrl\"\n [matAutocomplete]=\"auto\"\n [placeholder]=\"'yuv.smart-search.value.enter' | translate\"\n (keydown.enter)=\"onEnter()\"\n />\n }\n }\n\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"confirm-btn\"\n [disabled]=\"!ctrl.isConditionComplete()\"\n (click)=\"onEnter()\"\n [matTooltip]=\"'yuv.smart-search.confirm' | translate\"\n >\n <mat-icon>check</mat-icon>\n </button>\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"cancel-btn\"\n (click)=\"cancelPending()\"\n [matTooltip]=\"'yuv.smart-search.cancel' | translate\"\n >\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </ng-template>\n\n <!-- \u2500\u2500 Toolbar: Essentials \u21C4 Full Form mode switch (only when the dynamic feature is enabled) \u2500\u2500 -->\n @if (supportDynamicConditions() && (hasDynamicConditions() || formMode())) {\n <div class=\"toolbar\">\n <div class=\"toolbar__mode\" role=\"group\" [attr.aria-label]=\"'yuv.smart-search.mode.label' | translate\">\n <button\n type=\"button\"\n class=\"toolbar__mode-btn\"\n [class.toolbar__mode-btn--active]=\"formMode()\"\n [attr.aria-pressed]=\"formMode()\"\n (click)=\"enterFormMode()\"\n >\n {{ 'yuv.smart-search.mode.essentials' | translate }}\n </button>\n <button\n type=\"button\"\n class=\"toolbar__mode-btn\"\n [class.toolbar__mode-btn--active]=\"!formMode()\"\n [attr.aria-pressed]=\"!formMode()\"\n (click)=\"exitFormMode()\"\n >\n {{ 'yuv.smart-search.mode.full' | translate }}\n </button>\n </div>\n </div>\n }\n\n <!-- \u2500\u2500 Generated fill-out form of the dynamic conditions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (formMode()) {\n <div class=\"dynamic-form\">\n @for (grp of formBlocks(); track grp.block.id; let last = $last) {\n <div class=\"dynamic-form__block\">\n <!-- Muted header listing the block's target types (a block matches any of them). -->\n <div class=\"dynamic-form__block-header\">\n @for (t of grp.block.types; track t.id) {\n @if (!$first) {\n <span class=\"dynamic-form__type-sep\">{{ 'yuv.smart-search.combinator.or' | translate }}</span>\n }\n <span class=\"dynamic-form__type\">{{ t.label }}</span>\n }\n </div>\n\n <div class=\"dynamic-form__rows\">\n @for (f of grp.fields; track f.condition) {\n <div class=\"dynamic-form__row\">\n <span class=\"dynamic-form__label\">{{ f.condition.fieldLabel }}</span>\n <span class=\"dynamic-form__op\">{{ f.condition.operatorLabel }}</span>\n <div class=\"dynamic-form__value\">\n @if (f.def) {\n <yuv-metadata-form-field variant=\"raw\" [field]=\"f.def\" situation=\"EDIT\" [formControl]=\"f.control\" />\n } @else {\n <input [formControl]=\"f.control\" [placeholder]=\"'yuv.smart-search.value.enter' | translate\" />\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Blocks are type-scoped and always combine with OR (\"this type as well as that type\"). -->\n @if (!last) {\n <div class=\"combinator\">\n <span class=\"combinator__label\">{{ 'yuv.smart-search.combinator.as-well-as' | translate }}</span>\n </div>\n }\n } @empty {\n <span class=\"dynamic-form__empty\">{{ 'yuv.smart-search.mode.empty' | translate }}</span>\n }\n </div>\n } @else {\n <!-- \u2500\u2500 Full-text search bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"fulltext\" [class.muted]=\"!fulltext().term.trim() && ctrl.blocks().length\">\n <!-- Row 1: full-width term -->\n <div class=\"fulltext__term\">\n <mat-icon class=\"fulltext__icon\">search</mat-icon>\n <input\n class=\"fulltext__input\"\n [formControl]=\"fulltextTermCtrl\"\n [placeholder]=\"'yuv.smart-search.fulltext.placeholder' | translate\"\n />\n </div>\n\n <!-- Row 2: scope (single) + types (multiple) -->\n <div class=\"fulltext__filters\">\n <mat-select\n class=\"fulltext__scope\"\n [panelWidth]=\"null\"\n panelClass=\"smart-search-select-panel\"\n [value]=\"fulltext().scope\"\n (selectionChange)=\"setFulltextScope($event.value)\"\n [aria-label]=\"'yuv.smart-search.fulltext.scope.label' | translate\"\n >\n <mat-option value=\"all\">{{ 'yuv.smart-search.fulltext.scope.all' | translate }}</mat-option>\n <mat-option value=\"metadata\">{{ 'yuv.smart-search.fulltext.scope.metadata' | translate }}</mat-option>\n <mat-option value=\"content\">{{ 'yuv.smart-search.fulltext.scope.content' | translate }}</mat-option>\n </mat-select>\n\n <mat-select\n class=\"fulltext__types\"\n multiple\n [panelWidth]=\"null\"\n panelClass=\"smart-search-select-panel\"\n [value]=\"fulltextTypeSelection()\"\n (selectionChange)=\"onFulltextTypesChange($event.value)\"\n [aria-label]=\"'yuv.smart-search.fulltext.types.label' | translate\"\n >\n <mat-option [value]=\"ALL_TYPES\">{{ 'yuv.smart-search.fulltext.types.all' | translate }}</mat-option>\n @for (t of objectTypes(); track t.id) {\n <mat-option [value]=\"t.id\">{{ t.label ?? t.id }}</mat-option>\n }\n </mat-select>\n </div>\n </div>\n\n <!-- Joiner between the full-text unit and the condition blocks. Top-level units\n always combine with OR (\"looking for this as well as that\"), so this is a\n static label rather than a toggle. -->\n @if (fulltext().term.trim() && ctrl.blocks().length) {\n <div class=\"combinator\">\n <span class=\"combinator__label\">{{ 'yuv.smart-search.combinator.as-well-as' | translate }}</span>\n </div>\n }\n\n <!-- \u2500\u2500 Step 1: Type blocks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @for (block of ctrl.blocks(); track block.id) {\n <div class=\"block\" [attr.data-block-id]=\"block.id\" [class.block--active]=\"ctrl.activeBlock() === block\">\n <div class=\"block__header\">\n <div class=\"block__types\">\n @for (t of block.types; track t.id) {\n @if (!$first) {\n <span class=\"block__type-sep\">{{ 'yuv.smart-search.combinator.or' | translate }}</span>\n }\n <span class=\"block__type\">\n <span class=\"block__type-label\">{{ t.label }}</span>\n </span>\n }\n </div>\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"block__remove\"\n (click)=\"removeBlock(block)\"\n [matTooltip]=\"'yuv.smart-search.type.remove' | translate\"\n >\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <div class=\"block__conditions\">\n <yuv-smart-search-group [group]=\"block\" [bare]=\"true\" [chipTpl]=\"chipTpl\" [editorTpl]=\"editorTpl\" />\n </div>\n </div>\n\n <!-- Static joiner between blocks \u2014 blocks are type-scoped, so they always\n combine with OR (\"this type as well as that type\"). -->\n @if (!$last && ctrl.showCombinator()) {\n <div class=\"combinator\">\n <span class=\"combinator__label\">{{ 'yuv.smart-search.combinator.as-well-as' | translate }}</span>\n </div>\n }\n }\n\n <!-- \u2500\u2500 Step 1 input: add a type block (multi-select) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (ctrl.step() === 'type') {\n <div class=\"add-type-row\">\n <button\n type=\"button\"\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"add-type-row__icon\"\n (click)=\"typeInput.focus()\"\n [attr.aria-label]=\"'yuv.smart-search.add-type' | translate\"\n >\n <mat-icon>add_circle_outline</mat-icon>\n </button>\n\n <!-- Staged draft types as removable Material chips -->\n <mat-chip-grid\n #typeChipGrid\n class=\"add-type-row__chips\"\n [attr.aria-label]=\"'yuv.smart-search.add-type' | translate\"\n >\n @for (t of ctrl.draftTypes(); track t.id) {\n <mat-chip-row (removed)=\"ctrl.removeDraftType(t.id)\">\n {{ t.label }}\n <button matChipRemove [attr.aria-label]=\"'yuv.smart-search.type.remove-draft' | translate\">\n <mat-icon>close</mat-icon>\n </button>\n </mat-chip-row>\n }\n <input\n #typeInput\n [formControl]=\"ctrl.fieldCtrl\"\n [placeholder]=\"'yuv.smart-search.add-type' | translate\"\n [matAutocomplete]=\"auto\"\n [matChipInputFor]=\"typeChipGrid\"\n (keydown.enter)=\"onTypeEnter()\"\n (keydown.escape)=\"cancelPending()\"\n />\n </mat-chip-grid>\n\n @if (ctrl.draftTypes().length) {\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"add-type-row__confirm\"\n (click)=\"confirmTypes()\"\n [matTooltip]=\"'yuv.smart-search.type.confirm' | translate\"\n >\n <mat-icon>check</mat-icon>\n </button>\n }\n </div>\n }\n }\n\n <!-- \u2500\u2500 Shared autocomplete panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <mat-autocomplete\n #auto\n panelWidth=\"auto\"\n [displayWith]=\"displayFn\"\n (optionSelected)=\"onSuggestionSelected($event)\"\n (closed)=\"onPickerClosed()\"\n >\n @for (s of ctrl.suggestions(); track s.id) {\n <mat-option [value]=\"s\">\n <div class=\"suggestion\">\n @if (s.kind !== 'type') {\n <mat-icon class=\"suggestion__icon\">\n @switch (s.kind) {\n @case ('field') {\n tune\n }\n @case ('date-preset') {\n calendar_today\n }\n @default {\n manage_search\n }\n }\n </mat-icon>\n }\n <span class=\"suggestion__label\">{{ s.label }}</span>\n </div>\n </mat-option>\n }\n </mat-autocomplete>\n</div>\n", styles: [":host{display:block;--outline: rgb(from var(--ymt-text-color) r g b / .5);--focus-visible-border-color: var(--ymt-primary);--focus-visible-border-shadow-color: rgb(from var(--ymt-primary) r g b / .3)}::ng-deep .smart-search-select-panel.mat-mdc-select-panel{min-width:max-content}::ng-deep .smart-search-select-panel.mat-mdc-select-panel .mat-mdc-option .mdc-list-item__primary-text{white-space:nowrap}.smart-search{display:flex;flex-direction:column;gap:var(--ymt-spacing-xs)}.fulltext{display:flex;flex-direction:column;gap:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);border:1px solid var(--outline);border-radius:var(--ymt-corner-s);transition:opacity .15s}.fulltext.muted{opacity:.7}.fulltext.muted:hover,.fulltext.muted:focus-within{opacity:1}.fulltext__term{display:flex;align-items:center;gap:var(--ymt-spacing-xs)}.fulltext__icon{color:var(--ymt-text-color-subtle)}.fulltext__input{flex:1;min-width:120px;border:none;outline:none;background:transparent;font:inherit;color:var(--ymt-text-color)}.fulltext__input::placeholder{color:var(--ymt-text-color-subtle);opacity:.7}.fulltext__filters{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:var(--ymt-spacing-s)}.fulltext__scope,.fulltext__types{width:auto;font-size:.9em;color:var(--ymt-text-color-subtle);border:1px solid var(--ymt-outline);border-radius:var(--ymt-corner-xs);padding-inline:var(--ymt-spacing-xs)}.fulltext__scope:focus-visible,.fulltext__types:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.block{border:1px solid var(--outline);border-radius:var(--ymt-corner-s);overflow:hidden;transition:border-color .15s;padding:var(--ymt-spacing-2xs)}.block:focus-within{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 3px var(--focus-visible-border-shadow-color)}.block__header{border-radius:var(--ymt-corner-xs);display:flex;flex-wrap:wrap;align-items:center;gap:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);background:var(--ymt-surface-container-high)}.block__types{flex:1;display:flex;align-items:center;flex-wrap:wrap}.block__type{display:inline-flex;align-items:center;gap:var(--ymt-spacing-2xs)}.block__type-sep{font:var(--ymt-font-body-subtle);text-transform:lowercase;color:var(--ymt-text-color-subtle);padding:0 var(--ymt-spacing-xs)}.block__remove{margin-inline-start:auto;opacity:.7}.block__conditions{display:flex;flex-direction:column;align-items:stretch;gap:var(--ymt-spacing-2xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);min-height:44px}.condition-chip{display:flex;flex:1;align-items:center;gap:0;border-radius:var(--ymt-corner-xs);border:1px solid var(--outline);cursor:pointer;overflow:hidden;outline:none;gap:var(--ymt-spacing-2xs)}.condition-chip:hover{border-color:var(--ymt-outline)}.condition-chip:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.condition-chip__part{display:inline-flex;align-items:center;gap:var(--ymt-spacing-4xs);padding:0 var(--ymt-spacing-xs);height:100%;white-space:nowrap;color:var(--ymt-text-color)}.condition-chip__part:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.condition-chip__op{font:var(--ymt-font-body-subtle);color:var(--ymt-text-color-subtle);padding:0}.condition-chip__value{flex:1;overflow:hidden;text-overflow:ellipsis;min-height:24px;--tile-slot-padding: 0;--yuv-renderer-display: inline-flex}.condition-chip__remove{--mat-icon-button-container-shape: 0;color:var(--ymt-text-color-subtle)}.condition-chip__remove:hover{color:var(--ymt-text-color)}.condition-chip__placeholder{font:var(--ymt-font-body-subtle);font-style:italic;color:var(--ymt-text-color-subtle);opacity:.8}.condition-chip__dynamic{--mat-icon-button-container-shape: 0;color:var(--ymt-text-color-subtle)}.condition-chip__dynamic:hover{color:var(--ymt-text-color)}.condition-chip--dynamic{outline:2px solid var(--ymt-inverse-surface);outline-offset:1px}.condition-chip--dynamic .condition-chip__dynamic{border-radius:0;background-color:var(--ymt-inverse-surface);color:var(--ymt-on-inverse-surface)}.toolbar{display:flex;justify-content:flex-end;align-items:center;background:var(--ymt-surface-container-high);border-radius:var(--ymt-corner-xs);border:1px solid var(--ymt-outline);padding:var(--ymt-spacing-3xs)}.toolbar__mode{display:inline-flex;gap:0}.toolbar__mode-btn{--bg: transparent;--fg: var(--ymt-text-color);--mdc-shape-small: var(--ymt-corner-xs);font-size:.82em;background-color:var(--bg);color:var(--fg);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-xs);border:1px solid var(--ymt-inverse-surface);cursor:pointer}.toolbar__mode-btn:focus-visible{box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.toolbar__mode-btn:first-child{border-radius:var(--mdc-shape-small) 0 0 var(--mdc-shape-small)}.toolbar__mode-btn:last-child{border-radius:0 var(--mdc-shape-small) var(--mdc-shape-small) 0}.toolbar__mode-btn--active{--bg: var(--ymt-inverse-surface);--fg: var(--ymt-on-inverse-surface)}.dynamic-form{display:flex;flex-direction:column;gap:var(--ymt-spacing-xs)}.dynamic-form__block{container-type:inline-size;border:1px solid var(--outline);border-radius:var(--ymt-corner-s);overflow:hidden}.dynamic-form__block-header{display:flex;flex-wrap:wrap;align-items:center;gap:var(--ymt-spacing-xs);color:var(--ymt-text-color-subtle);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-s);font:var(--ymt-font-body-subtle)}.dynamic-form__type-sep{text-transform:lowercase;color:var(--ymt-text-color-subtle);padding:0 var(--ymt-spacing-2xs)}.dynamic-form__rows{display:grid;grid-template-columns:max-content max-content 1fr;row-gap:var(--ymt-spacing-s);column-gap:var(--ymt-spacing-xs);overflow:auto;padding:var(--ymt-spacing-s)}.dynamic-form__row{display:grid;grid-template-columns:subgrid;grid-column:1/-1;align-items:center}.dynamic-form__label{color:var(--ymt-text-color)}.dynamic-form__op{font:var(--ymt-font-body-subtle);color:var(--ymt-text-color-subtle);border:1px solid var(--outline);padding-inline:var(--ymt-spacing-2xs);border-radius:var(--ymt-corner-xs)}.dynamic-form__value{flex:1;padding:var(--ymt-spacing-xs);border:1px solid var(--outline);border-radius:var(--ymt-corner-xs)}.dynamic-form__value input{width:100%;border:none;outline:none;background:transparent;font:inherit;color:var(--ymt-text-color)}.dynamic-form__empty{font:var(--ymt-font-body-subtle);color:var(--ymt-text-color-subtle)}@container (max-width: 499px){.dynamic-form__rows{grid-template-columns:1fr auto}.dynamic-form__row{grid-template-columns:subgrid;grid-column:1/-1;row-gap:var(--ymt-spacing-2xs)}.dynamic-form__value{grid-column:1/-1}}.inline-input{display:flex;align-items:center;gap:var(--ymt-spacing-2xs);flex:1;border:1px dashed var(--outline);border-radius:var(--ymt-corner-xs);padding:var(--ymt-spacing-2xs);background:var(--ymt-surface)}.inline-input yuv-metadata-form-field{border-radius:var(--ymt-corner-xs)}.inline-input yuv-metadata-form-field:focus-within{outline:1px solid var(--focus-visible-border-color)}.inline-input .part-pill{display:inline-flex;align-items:center;gap:var(--ymt-spacing-4xs);white-space:nowrap;font:inherit;font-size:var(--ymt-font-body-subtle-size);color:var(--ymt-text-color);background:var(--ymt-surface-container);border:1px solid var(--ymt-outline);border-radius:var(--ymt-corner-xs);padding:var(--ymt-spacing-4xs) var(--ymt-spacing-xs);cursor:pointer;transition:border-color .1s,background .1s}.inline-input .part-pill:hover{background:var(--ymt-surface-container-high)}.inline-input .part-pill:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.inline-input .part-pill--active{border-color:var(--ymt-primary);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.inline-input .part-pill mat-icon{font-size:var(--ymt-sizing-xs);width:var(--ymt-sizing-xs);height:var(--ymt-sizing-xs)}.inline-input yuv-metadata-form-field{flex:1;min-width:150px}.inline-input input{flex:1;border:none;outline:none;background:transparent}.inline-input .confirm-btn:not(:disabled){opacity:1;color:var(--ymt-color-accent)}.combinator{display:flex;justify-content:center;gap:0}.combinator__label{font:var(--ymt-font-body-subtle);text-transform:lowercase;color:var(--ymt-text-color-subtle);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-xs)}.add-type-row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);border:1px dashed var(--ymt-outline);border-radius:var(--ymt-corner-s);color:var(--ymt-text-color-subtle)}.add-type-row__icon{display:inline-flex;align-items:center;padding:0;border:0;background:transparent;color:inherit;cursor:pointer}.add-type-row__icon mat-icon{font-size:var(--ymt-sizing-m);width:var(--ymt-sizing-m);height:var(--ymt-sizing-m);opacity:.5}.add-type-row__icon:hover mat-icon{opacity:.8}.add-type-row__confirm{color:var(--ymt-primary)}.add-type-row__chips{flex:1;min-width:120px}.add-type-row input{flex:1;min-width:120px;border:none;outline:none;background:transparent;font:inherit;font-size:.9em;color:inherit}.add-type-row input::placeholder{color:var(--ymt-text-color-subtle);opacity:.7}.condition-combinator{display:flex;align-self:center}.suggestion{display:flex;align-items:center;gap:var(--ymt-spacing-xs)}.suggestion__icon{font-size:var(--ymt-sizing-s);width:var(--ymt-sizing-s);height:var(--ymt-sizing-s);--icon-size: var(--ymt-sizing-s);opacity:.6}.suggestion__label{flex:1}\n"] }]
|
|
2145
|
-
}], ctorParameters: () => [], propDecorators: { types: [{ type: i0.Input, args: [{ isSignal: true, alias: "types", required: false }] }], skipProperties: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipProperties", required: false }] }], supportDynamicConditions: [{ type: i0.Input, args: [{ isSignal: true, alias: "supportDynamicConditions", required: false }] }], queryChange: [{ type: i0.Output, args: ["queryChange"] }], auto: [{ type: i0.ViewChild, args: ['auto', { isSignal: true }] }], trigger: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MatAutocompleteTrigger), { isSignal: true }] }] } });
|
|
2468
|
+
], providers: [SmartSearchEditController], host: { '(keydown.enter)': 'onHostEnter($event)' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"smart-search\" halo-container halo-container-skip=\"true\">\n <!-- Condition chip template \u2014 passed down into the recursive group component. -->\n <ng-template #chipTpl let-condition let-container=\"container\" let-i=\"index\">\n <div\n class=\"condition-chip\"\n [class.condition-chip--unset]=\"isUnset(condition)\"\n [class.condition-chip--dynamic]=\"condition.dynamic\"\n tabindex=\"0\"\n role=\"button\"\n [attr.aria-label]=\"'yuv.smart-search.condition.edit-aria' | translate: { label: condition.conditionLabel }\"\n (click)=\"editCondition(container, condition, i)\"\n (keydown.space)=\"$event.preventDefault(); editCondition(container, condition, i)\"\n (keydown.enter)=\"$event.preventDefault(); editCondition(container, condition, i)\"\n >\n @if (supportDynamicConditions() && !isValueless(condition.operator)) {\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"condition-chip__dynamic\"\n tabindex=\"-1\"\n [attr.aria-pressed]=\"!!condition.dynamic\"\n [matTooltip]=\"\n (condition.dynamic\n ? 'yuv.smart-search.condition.unmark-dynamic'\n : 'yuv.smart-search.condition.mark-dynamic'\n ) | translate\n \"\n (click)=\"$event.stopPropagation(); toggleDynamic(container, condition)\"\n >\n <mat-icon>bolt</mat-icon>\n </button>\n }\n\n <span class=\"condition-chip__part condition-chip__field\">\n <!-- <mat-icon>tune</mat-icon> -->\n {{ condition.fieldLabel }}\n </span>\n <span class=\"condition-chip__part condition-chip__op\">{{ condition.operatorLabel }}</span>\n <!-- A valueless operator states the whole condition on its own (`is empty`, or a date\n preset whose label already reads \u201CThis year\u201D), so it gets no value segment: the\n stored value is an internal marker (`thisYear`), not something to show the user. -->\n @if (!isValueless(condition.operator)) {\n <span class=\"condition-chip__part condition-chip__value\">\n @if (isUnset(condition)) {\n <span class=\"condition-chip__placeholder\">{{ 'yuv.smart-search.condition.unset-value' | translate }}</span>\n } @else if (valueRendererInput(condition); as rendererInput) {\n <ng-container *yuvRenderer=\"rendererInput\" />\n } @else {\n {{ condition.value }}\n }\n </span>\n }\n <!-- @if (supportDynamicConditions() && !isValueless(condition.operator)) {\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"condition-chip__dynamic\"\n tabindex=\"-1\"\n [attr.aria-pressed]=\"!!condition.dynamic\"\n [matTooltip]=\"\n (condition.dynamic\n ? 'yuv.smart-search.condition.unmark-dynamic'\n : 'yuv.smart-search.condition.mark-dynamic'\n ) | translate\n \"\n (click)=\"$event.stopPropagation(); toggleDynamic(container, condition)\"\n >\n <mat-icon>bolt</mat-icon>\n </button>\n } -->\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"condition-chip__remove\"\n tabindex=\"-1\"\n [attr.aria-label]=\"'yuv.smart-search.condition.remove' | translate\"\n (click)=\"$event.stopPropagation(); removeCondition(container, condition)\"\n >\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </ng-template>\n\n <!-- Inline editor template \u2014 rendered inside whichever container is active. -->\n <ng-template #editorTpl>\n <!-- Enter is bound on the wrapper, not per input, so it also covers the value step's\n real metadata widget and runs after each widget's own key handling. -->\n <div\n class=\"inline-input\"\n tabindex=\"-1\"\n (focusout)=\"onInlineBlur($event)\"\n (keydown.enter)=\"onEnter($event)\"\n (keydown.escape)=\"cancelPending()\"\n >\n @if (ctrl.step() !== 'field' && ctrl.pendingField()) {\n <button\n class=\"part-pill\"\n [class.part-pill--active]=\"ctrl.step() === 'field'\"\n (click)=\"editField()\"\n [matTooltip]=\"'yuv.smart-search.field.change' | translate\"\n >\n {{ ctrl.pendingField()?.label }}\n </button>\n }\n\n <!-- The operator is always a pill, never editable text: at the value step it is the\n way back to the operator picker, and at the operator step it shows what is\n currently set while the (empty) input below filters the replacement list. -->\n @if (ctrl.step() !== 'field' && ctrl.pendingOperatorLabel()) {\n <button\n class=\"part-pill\"\n [class.part-pill--active]=\"ctrl.step() === 'operator'\"\n (click)=\"editOperator()\"\n [matTooltip]=\"'yuv.smart-search.operator.change' | translate\"\n >\n {{ ctrl.pendingOperatorLabel() }}\n </button>\n }\n\n @if (ctrl.step() === 'field') {\n <input\n [formControl]=\"ctrl.fieldCtrl\"\n [matAutocomplete]=\"auto\"\n [placeholder]=\"'yuv.smart-search.field.pick' | translate\"\n />\n } @else if (ctrl.step() === 'operator') {\n <input\n [formControl]=\"ctrl.operatorCtrl\"\n [matAutocomplete]=\"auto\"\n [placeholder]=\"'yuv.smart-search.operator.select' | translate\"\n />\n } @else if (ctrl.step() === 'value') {\n @let otf = valueFieldDef();\n @if (otf) {\n <yuv-metadata-form-field variant=\"raw\" [field]=\"otf\" situation=\"EDIT\" [formControl]=\"ctrl.valueCtrl\" />\n } @else {\n <input\n [formControl]=\"ctrl.valueCtrl\"\n [matAutocomplete]=\"auto\"\n [placeholder]=\"'yuv.smart-search.value.enter' | translate\"\n />\n }\n }\n\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"confirm-btn\"\n [disabled]=\"!ctrl.isConditionComplete()\"\n (click)=\"onEnter()\"\n [matTooltip]=\"'yuv.smart-search.confirm' | translate\"\n >\n <mat-icon>check</mat-icon>\n </button>\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"cancel-btn\"\n (click)=\"cancelPending()\"\n [matTooltip]=\"'yuv.smart-search.cancel' | translate\"\n >\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </ng-template>\n\n <!-- \u2500\u2500 Toolbar: Essentials \u21C4 Full Form mode switch (only when the dynamic feature is enabled) \u2500\u2500 -->\n @if (!fulltextOnly() && supportDynamicConditions() && (hasDynamicConditions() || formMode())) {\n <div class=\"toolbar\">\n <div class=\"toolbar__mode\" role=\"group\" [attr.aria-label]=\"'yuv.smart-search.mode.label' | translate\">\n <button\n type=\"button\"\n class=\"toolbar__mode-btn\"\n [class.toolbar__mode-btn--active]=\"formMode()\"\n [attr.aria-pressed]=\"formMode()\"\n (click)=\"enterFormMode()\"\n >\n {{ 'yuv.smart-search.mode.essentials' | translate }}\n </button>\n <button\n type=\"button\"\n class=\"toolbar__mode-btn\"\n [class.toolbar__mode-btn--active]=\"!formMode()\"\n [attr.aria-pressed]=\"!formMode()\"\n (click)=\"exitFormMode()\"\n >\n {{ 'yuv.smart-search.mode.full' | translate }}\n </button>\n </div>\n </div>\n }\n\n <!-- \u2500\u2500 Generated fill-out form of the dynamic conditions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (formMode()) {\n <div class=\"dynamic-form\">\n @for (grp of formBlocks(); track grp.block.id; let last = $last) {\n <div class=\"dynamic-form__block\">\n <!-- Muted header listing the block's target types (a block matches any of them). -->\n <div class=\"dynamic-form__block-header\">\n @for (t of grp.block.types; track t.id) {\n @if (!$first) {\n <span class=\"dynamic-form__type-sep\">{{ 'yuv.smart-search.combinator.or' | translate }}</span>\n }\n <span class=\"dynamic-form__type\">{{ t.label }}</span>\n }\n </div>\n\n <div class=\"dynamic-form__rows\">\n @for (f of grp.fields; track f.condition) {\n <div class=\"dynamic-form__row\">\n <span class=\"dynamic-form__label\">{{ f.condition.fieldLabel }}</span>\n <span class=\"dynamic-form__op\">{{ f.condition.operatorLabel }}</span>\n <div class=\"dynamic-form__value\">\n @if (f.def) {\n <yuv-metadata-form-field variant=\"raw\" [field]=\"f.def\" situation=\"EDIT\" [formControl]=\"f.control\" />\n } @else {\n <input [formControl]=\"f.control\" [placeholder]=\"'yuv.smart-search.value.enter' | translate\" />\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Blocks are type-scoped and always combine with OR (\"this type as well as that type\"). -->\n @if (!last) {\n <div class=\"combinator\">\n <span class=\"combinator__label\">{{ 'yuv.smart-search.combinator.as-well-as' | translate }}</span>\n </div>\n }\n } @empty {\n <span class=\"dynamic-form__empty\">{{ 'yuv.smart-search.mode.empty' | translate }}</span>\n }\n </div>\n } @else {\n <!-- \u2500\u2500 Full-text search bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Muting hints \"the blocks carry the search, not me\" \u2014 with the blocks hidden there\n is nothing to defer to, so the only visible control never greys out. -->\n <div class=\"fulltext\" [class.muted]=\"!fulltextOnly() && !fulltext().term.trim() && ctrl.blocks().length\">\n <!-- Row 1: full-width term -->\n <div class=\"fulltext__term\">\n <mat-icon class=\"fulltext__icon\">search</mat-icon>\n <input\n class=\"fulltext__input\"\n [formControl]=\"fulltextTermCtrl\"\n [placeholder]=\"'yuv.smart-search.fulltext.placeholder' | translate\"\n />\n </div>\n\n <!-- Row 2: scope (single) + types (multiple) -->\n <div class=\"fulltext__filters\">\n <mat-select\n class=\"fulltext__scope\"\n [panelWidth]=\"null\"\n panelClass=\"smart-search-select-panel\"\n [value]=\"fulltext().scope\"\n (selectionChange)=\"setFulltextScope($event.value)\"\n [aria-label]=\"'yuv.smart-search.fulltext.scope.label' | translate\"\n >\n <mat-option value=\"all\">{{ 'yuv.smart-search.fulltext.scope.all' | translate }}</mat-option>\n <mat-option value=\"metadata\">{{ 'yuv.smart-search.fulltext.scope.metadata' | translate }}</mat-option>\n <mat-option value=\"content\">{{ 'yuv.smart-search.fulltext.scope.content' | translate }}</mat-option>\n </mat-select>\n\n <mat-select\n class=\"fulltext__types\"\n multiple\n [panelWidth]=\"null\"\n panelClass=\"smart-search-select-panel\"\n [value]=\"fulltextTypeSelection()\"\n (selectionChange)=\"onFulltextTypesChange($event.value)\"\n [aria-label]=\"'yuv.smart-search.fulltext.types.label' | translate\"\n >\n <mat-option [value]=\"ALL_TYPES\">{{ 'yuv.smart-search.fulltext.types.all' | translate }}</mat-option>\n @for (t of objectTypes(); track t.id) {\n <mat-option [value]=\"t.id\">{{ t.label ?? t.id }}</mat-option>\n }\n </mat-select>\n </div>\n </div>\n\n <!-- \u2500\u2500 Condition builder (hidden in full-text-only mode) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (!fulltextOnly()) {\n <!-- Joiner between the full-text unit and the condition blocks. Top-level units\n always combine with OR (\"looking for this as well as that\"), so this is a\n static label rather than a toggle. -->\n @if (fulltext().term.trim() && ctrl.blocks().length) {\n <div class=\"combinator\">\n <span class=\"combinator__label\">{{ 'yuv.smart-search.combinator.as-well-as' | translate }}</span>\n </div>\n }\n\n <!-- \u2500\u2500 Step 1: Type blocks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @for (block of ctrl.blocks(); track block.id) {\n <div class=\"block\" [attr.data-block-id]=\"block.id\" [class.block--active]=\"ctrl.activeBlock() === block\">\n <div class=\"block__header\">\n <div class=\"block__types\">\n @for (t of block.types; track t.id) {\n @if (!$first) {\n <span class=\"block__type-sep\">{{ 'yuv.smart-search.combinator.or' | translate }}</span>\n }\n <span class=\"block__type\">\n <span class=\"block__type-label\">{{ t.label }}</span>\n </span>\n }\n </div>\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"block__remove\"\n (click)=\"removeBlock(block)\"\n [matTooltip]=\"'yuv.smart-search.type.remove' | translate\"\n >\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <div class=\"block__conditions\">\n <yuv-smart-search-group [group]=\"block\" [bare]=\"true\" [chipTpl]=\"chipTpl\" [editorTpl]=\"editorTpl\" />\n </div>\n </div>\n\n <!-- Static joiner between blocks \u2014 blocks are type-scoped, so they always\n combine with OR (\"this type as well as that type\"). -->\n @if (!$last && ctrl.showCombinator()) {\n <div class=\"combinator\">\n <span class=\"combinator__label\">{{ 'yuv.smart-search.combinator.as-well-as' | translate }}</span>\n </div>\n }\n }\n\n <!-- \u2500\u2500 Step 1 input: add a type block (multi-select) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (ctrl.step() === 'type') {\n <div class=\"add-type-row\">\n <button\n type=\"button\"\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"add-type-row__icon\"\n (click)=\"typeInput.focus()\"\n [attr.aria-label]=\"'yuv.smart-search.add-type' | translate\"\n >\n <mat-icon>add_circle_outline</mat-icon>\n </button>\n\n <!-- Staged draft types as removable Material chips -->\n <mat-chip-grid\n #typeChipGrid\n class=\"add-type-row__chips\"\n [attr.aria-label]=\"'yuv.smart-search.add-type' | translate\"\n >\n @for (t of ctrl.draftTypes(); track t.id) {\n <mat-chip-row (removed)=\"ctrl.removeDraftType(t.id)\">\n {{ t.label }}\n <button matChipRemove [attr.aria-label]=\"'yuv.smart-search.type.remove-draft' | translate\">\n <mat-icon>close</mat-icon>\n </button>\n </mat-chip-row>\n }\n <input\n #typeInput\n [formControl]=\"ctrl.fieldCtrl\"\n [placeholder]=\"'yuv.smart-search.add-type' | translate\"\n [matAutocomplete]=\"auto\"\n [matChipInputFor]=\"typeChipGrid\"\n [matChipInputSeparatorKeyCodes]=\"NO_SEPARATOR_KEYS\"\n (keydown.enter)=\"onTypeEnter($event)\"\n (keydown.escape)=\"cancelPending()\"\n />\n </mat-chip-grid>\n\n @if (ctrl.draftTypes().length) {\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"add-type-row__confirm\"\n (click)=\"confirmTypes()\"\n [matTooltip]=\"'yuv.smart-search.type.confirm' | translate\"\n >\n <mat-icon>check</mat-icon>\n </button>\n }\n </div>\n }\n }\n }\n\n <!-- \u2500\u2500 Shared autocomplete panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <mat-autocomplete\n #auto\n panelWidth=\"auto\"\n [displayWith]=\"displayFn\"\n (optionSelected)=\"onSuggestionSelected($event)\"\n (closed)=\"onPickerClosed()\"\n >\n @for (s of ctrl.suggestions(); track s.id) {\n <mat-option [value]=\"s\">\n <div class=\"suggestion\">\n @if (s.kind !== 'type') {\n <mat-icon class=\"suggestion__icon\">\n @switch (s.kind) {\n @case ('field') {\n tune\n }\n @case ('date-preset') {\n calendar_today\n }\n @default {\n manage_search\n }\n }\n </mat-icon>\n }\n <span class=\"suggestion__label\">{{ s.label }}</span>\n </div>\n </mat-option>\n }\n </mat-autocomplete>\n</div>\n", styles: [":host{display:block;--outline: rgb(from var(--ymt-text-color) r g b / .5);--focus-visible-border-color: var(--ymt-primary);--focus-visible-border-shadow-color: rgb(from var(--ymt-primary) r g b / .3)}::ng-deep .smart-search-select-panel.mat-mdc-select-panel{min-width:max-content}::ng-deep .smart-search-select-panel.mat-mdc-select-panel .mat-mdc-option .mdc-list-item__primary-text{white-space:nowrap}.smart-search{display:flex;flex-direction:column;gap:var(--ymt-spacing-xs)}.fulltext{display:flex;flex-direction:column;gap:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);border:1px solid var(--outline);border-radius:var(--ymt-corner-s);transition:opacity .15s}.fulltext.muted{opacity:.7}.fulltext.muted:hover,.fulltext.muted:focus-within{opacity:1}.fulltext__term{display:flex;align-items:center;gap:var(--ymt-spacing-xs)}.fulltext__icon{color:var(--ymt-text-color-subtle)}.fulltext__input{flex:1;min-width:120px;border:none;outline:none;background:transparent;font:inherit;color:var(--ymt-text-color)}.fulltext__input::placeholder{color:var(--ymt-text-color-subtle);opacity:.7}.fulltext__filters{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:var(--ymt-spacing-s)}.fulltext__scope,.fulltext__types{width:auto;font-size:.9em;color:var(--ymt-text-color-subtle);border:1px solid var(--ymt-outline);border-radius:var(--ymt-corner-xs);padding-inline:var(--ymt-spacing-xs)}.fulltext__scope:focus-visible,.fulltext__types:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.block{border:1px solid var(--outline);border-radius:var(--ymt-corner-s);overflow:hidden;transition:border-color .15s;padding:var(--ymt-spacing-2xs)}.block:focus-within{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 3px var(--focus-visible-border-shadow-color)}.block__header{border-radius:var(--ymt-corner-xs);display:flex;flex-wrap:wrap;align-items:center;gap:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);background:var(--ymt-surface-container-high)}.block__types{flex:1;display:flex;align-items:center;flex-wrap:wrap}.block__type{display:inline-flex;align-items:center;gap:var(--ymt-spacing-2xs)}.block__type-sep{font:var(--ymt-font-body-subtle);text-transform:lowercase;color:var(--ymt-text-color-subtle);padding:0 var(--ymt-spacing-xs)}.block__remove{margin-inline-start:auto;opacity:.7}.block__conditions{display:flex;flex-direction:column;align-items:stretch;gap:var(--ymt-spacing-2xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);min-height:44px}.condition-chip{display:flex;flex:1;align-items:center;gap:0;border-radius:var(--ymt-corner-xs);border:1px solid var(--outline);cursor:pointer;overflow:hidden;outline:none;gap:var(--ymt-spacing-2xs)}.condition-chip:hover{border-color:var(--ymt-outline)}.condition-chip:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.condition-chip__part{display:inline-flex;align-items:center;gap:var(--ymt-spacing-4xs);padding:0 var(--ymt-spacing-xs);height:100%;white-space:nowrap;color:var(--ymt-text-color)}.condition-chip__part:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.condition-chip__op{font:var(--ymt-font-body-subtle);color:var(--ymt-text-color-subtle);padding:0}.condition-chip__value{flex:1;overflow:hidden;text-overflow:ellipsis;min-height:24px;--tile-slot-padding: 0;--yuv-renderer-display: inline-flex}.condition-chip__remove{--mat-icon-button-container-shape: 0;color:var(--ymt-text-color-subtle)}.condition-chip__remove:hover{color:var(--ymt-text-color)}.condition-chip__placeholder{font:var(--ymt-font-body-subtle);font-style:italic;color:var(--ymt-text-color-subtle);opacity:.8}.condition-chip__dynamic{--mat-icon-button-container-shape: 0;color:var(--ymt-text-color-subtle)}.condition-chip__dynamic:hover{color:var(--ymt-text-color)}.condition-chip--dynamic{outline:2px solid var(--ymt-inverse-surface);outline-offset:1px}.condition-chip--dynamic .condition-chip__dynamic{border-radius:0;background-color:var(--ymt-inverse-surface);color:var(--ymt-on-inverse-surface)}.toolbar{display:flex;justify-content:flex-end;align-items:center;background:var(--ymt-surface-container-high);border-radius:var(--ymt-corner-xs);border:1px solid var(--ymt-outline);padding:var(--ymt-spacing-3xs)}.toolbar__mode{display:inline-flex;gap:0}.toolbar__mode-btn{--bg: transparent;--fg: var(--ymt-text-color);--mdc-shape-small: var(--ymt-corner-xs);font-size:.82em;background-color:var(--bg);color:var(--fg);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-xs);border:1px solid var(--ymt-inverse-surface);cursor:pointer}.toolbar__mode-btn:focus-visible{box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.toolbar__mode-btn:first-child{border-radius:var(--mdc-shape-small) 0 0 var(--mdc-shape-small)}.toolbar__mode-btn:last-child{border-radius:0 var(--mdc-shape-small) var(--mdc-shape-small) 0}.toolbar__mode-btn--active{--bg: var(--ymt-inverse-surface);--fg: var(--ymt-on-inverse-surface)}.dynamic-form{display:flex;flex-direction:column;gap:var(--ymt-spacing-xs)}.dynamic-form__block{container-type:inline-size;border:1px solid var(--outline);border-radius:var(--ymt-corner-s);overflow:hidden}.dynamic-form__block-header{display:flex;flex-wrap:wrap;align-items:center;gap:var(--ymt-spacing-xs);color:var(--ymt-text-color-subtle);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-s);font:var(--ymt-font-body-subtle)}.dynamic-form__type-sep{text-transform:lowercase;color:var(--ymt-text-color-subtle);padding:0 var(--ymt-spacing-2xs)}.dynamic-form__rows{display:grid;grid-template-columns:max-content max-content 1fr;row-gap:var(--ymt-spacing-s);column-gap:var(--ymt-spacing-xs);overflow:auto;padding:var(--ymt-spacing-s)}.dynamic-form__row{display:grid;grid-template-columns:subgrid;grid-column:1/-1;align-items:center}.dynamic-form__label{color:var(--ymt-text-color)}.dynamic-form__op{font:var(--ymt-font-body-subtle);color:var(--ymt-text-color-subtle);border:1px solid var(--outline);padding-inline:var(--ymt-spacing-2xs);border-radius:var(--ymt-corner-xs)}.dynamic-form__value{flex:1;padding:var(--ymt-spacing-xs);border:1px solid var(--outline);border-radius:var(--ymt-corner-xs)}.dynamic-form__value input{width:100%;border:none;outline:none;background:transparent;font:inherit;color:var(--ymt-text-color)}.dynamic-form__empty{font:var(--ymt-font-body-subtle);color:var(--ymt-text-color-subtle)}@container (max-width: 499px){.dynamic-form__rows{grid-template-columns:1fr auto}.dynamic-form__row{grid-template-columns:subgrid;grid-column:1/-1;row-gap:var(--ymt-spacing-2xs)}.dynamic-form__value{grid-column:1/-1}}.inline-input{display:flex;align-items:center;gap:var(--ymt-spacing-2xs);flex:1;border:1px dashed var(--outline);border-radius:var(--ymt-corner-xs);padding:var(--ymt-spacing-2xs);background:var(--ymt-surface)}.inline-input yuv-metadata-form-field{border-radius:var(--ymt-corner-xs)}.inline-input yuv-metadata-form-field:focus-within{outline:1px solid var(--focus-visible-border-color)}.inline-input .part-pill{display:inline-flex;align-items:center;gap:var(--ymt-spacing-4xs);white-space:nowrap;font:inherit;font-size:var(--ymt-font-body-subtle-size);color:var(--ymt-text-color);background:var(--ymt-surface-container);border:1px solid var(--ymt-outline);border-radius:var(--ymt-corner-xs);padding:var(--ymt-spacing-4xs) var(--ymt-spacing-xs);cursor:pointer;transition:border-color .1s,background .1s}.inline-input .part-pill:hover{background:var(--ymt-surface-container-high)}.inline-input .part-pill:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.inline-input .part-pill--active{border-color:var(--ymt-primary);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.inline-input .part-pill mat-icon{font-size:var(--ymt-sizing-xs);width:var(--ymt-sizing-xs);height:var(--ymt-sizing-xs)}.inline-input yuv-metadata-form-field{flex:1;min-width:150px}.inline-input input{flex:1;border:none;outline:none;background:transparent}.inline-input .confirm-btn:not(:disabled){opacity:1;color:var(--ymt-color-accent)}.combinator{display:flex;justify-content:center;gap:0}.combinator__label{font:var(--ymt-font-body-subtle);text-transform:lowercase;color:var(--ymt-text-color-subtle);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-xs)}.add-type-row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);border:1px dashed var(--ymt-outline);border-radius:var(--ymt-corner-s);color:var(--ymt-text-color-subtle)}.add-type-row__icon{display:inline-flex;align-items:center;padding:0;border:0;background:transparent;color:inherit;cursor:pointer}.add-type-row__icon mat-icon{font-size:var(--ymt-sizing-m);width:var(--ymt-sizing-m);height:var(--ymt-sizing-m);opacity:.5}.add-type-row__icon:hover mat-icon{opacity:.8}.add-type-row__confirm{color:var(--ymt-primary)}.add-type-row__chips{flex:1;min-width:120px}.add-type-row input{flex:1;min-width:120px;border:none;outline:none;background:transparent;font:inherit;font-size:.9em;color:inherit}.add-type-row input::placeholder{color:var(--ymt-text-color-subtle);opacity:.7}.condition-combinator{display:flex;align-self:center}.suggestion{display:flex;align-items:center;gap:var(--ymt-spacing-xs)}.suggestion__icon{font-size:var(--ymt-sizing-s);width:var(--ymt-sizing-s);height:var(--ymt-sizing-s);--icon-size: var(--ymt-sizing-s);opacity:.6}.suggestion__label{flex:1}\n"] }]
|
|
2469
|
+
}], ctorParameters: () => [], propDecorators: { types: [{ type: i0.Input, args: [{ isSignal: true, alias: "types", required: false }] }], skipProperties: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipProperties", required: false }] }], fulltextOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "fulltextOnly", required: false }] }], supportDynamicConditions: [{ type: i0.Input, args: [{ isSignal: true, alias: "supportDynamicConditions", required: false }] }], queryChange: [{ type: i0.Output, args: ["queryChange"] }], querySubmit: [{ type: i0.Output, args: ["querySubmit"] }], auto: [{ type: i0.ViewChild, args: ['auto', { isSignal: true }] }], trigger: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MatAutocompleteTrigger), { isSignal: true }] }] } });
|
|
2146
2470
|
|
|
2147
2471
|
/**
|
|
2148
2472
|
* Convenience NgModule that imports and re-exports {@link SmartSearchComponent}.
|