@yuuvis/client-framework 3.18.0 → 3.20.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-forms.mjs +4 -2
- package/fesm2022/yuuvis-client-framework-forms.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-object-details.mjs +2 -1
- package/fesm2022/yuuvis-client-framework-object-details.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-object-flavor.mjs +2 -1
- package/fesm2022/yuuvis-client-framework-object-flavor.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-object-form.mjs +3 -1
- package/fesm2022/yuuvis-client-framework-object-form.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-object-relationship.mjs +9 -3
- package/fesm2022/yuuvis-client-framework-object-relationship.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-object-summary.mjs +4 -1
- package/fesm2022/yuuvis-client-framework-object-summary.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-renderer.mjs +100 -16
- package/fesm2022/yuuvis-client-framework-renderer.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-smart-search.mjs +433 -69
- package/fesm2022/yuuvis-client-framework-smart-search.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-tile-list.mjs +2 -2
- package/fesm2022/yuuvis-client-framework-tile-list.mjs.map +1 -1
- package/package.json +5 -5
- package/smart-search/README.md +57 -7
- package/types/yuuvis-client-framework-object-form.d.ts +6 -0
- package/types/yuuvis-client-framework-object-preview.d.ts +14 -1
- package/types/yuuvis-client-framework-renderer.d.ts +40 -6
- package/types/yuuvis-client-framework-smart-search.d.ts +171 -22
- 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
|
|
@@ -321,17 +335,41 @@ const FULLTEXT_SCOPE_COLUMN = {
|
|
|
321
335
|
};
|
|
322
336
|
/**
|
|
323
337
|
* Build the whole-object full-text clause: a single `CONTAINS('term')` predicate, optionally scoped
|
|
324
|
-
* to a column and AND-ed with a type restriction.
|
|
338
|
+
* to a column and AND-ed with a type restriction.
|
|
339
|
+
*
|
|
340
|
+
* A type restriction on its own — types picked in the full-text bar's type filter with nothing
|
|
341
|
+
* typed — is a query in its own right ("show me everything of this type"), exactly like a type
|
|
342
|
+
* block carrying no conditions. So the picked types are returned alone in that case; only a unit
|
|
343
|
+
* with neither a term nor a picked type contributes nothing and yields `''`.
|
|
344
|
+
*
|
|
345
|
+
* @param fulltext The full-text unit. An empty `types` list means "all types".
|
|
346
|
+
* @param scopeTypes The types the search is scoped to (the host's `types` allow-list). When the
|
|
347
|
+
* unit picks no concrete type, "all types" means *any of these* — so the restriction is still
|
|
348
|
+
* emitted. Pass an empty list for an unscoped search: then "all types" restricts nothing.
|
|
349
|
+
*
|
|
350
|
+
* The scope deliberately only narrows a *term* search. With nothing typed and no type picked the
|
|
351
|
+
* panel is simply untouched, and falling back to the scope there would turn it into a standing
|
|
352
|
+
* "everything in scope" query the moment it opens.
|
|
325
353
|
*/
|
|
326
|
-
function buildFulltextClause(fulltext) {
|
|
354
|
+
function buildFulltextClause(fulltext, scopeTypes = []) {
|
|
327
355
|
const term = fulltext.term.trim();
|
|
356
|
+
const pickedTypeCond = buildTypeClause(fulltext.types);
|
|
328
357
|
if (!term)
|
|
329
|
-
return
|
|
358
|
+
return pickedTypeCond;
|
|
330
359
|
const column = FULLTEXT_SCOPE_COLUMN[fulltext.scope];
|
|
331
360
|
const contains = column ? `${column} CONTAINS('${escapeCmis(term)}')` : `CONTAINS('${escapeCmis(term)}')`;
|
|
332
|
-
const typeCond =
|
|
361
|
+
const typeCond = fulltext.types.length > 0 ? pickedTypeCond : buildTypeClause(scopeTypes);
|
|
333
362
|
return typeCond ? `(${typeCond} AND ${contains})` : contains;
|
|
334
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* Whether the full-text unit contributes a clause to the query — a term, a type restriction, or
|
|
366
|
+
* both. Lives next to {@link buildFulltextClause} so the UI's notion of "this unit is active"
|
|
367
|
+
* (its muted styling, the joiner to the blocks below it, the unit count behind the AND/OR toggle)
|
|
368
|
+
* cannot drift from what actually ends up in the query.
|
|
369
|
+
*/
|
|
370
|
+
function isFulltextActive(fulltext) {
|
|
371
|
+
return !!fulltext.term.trim() || fulltext.types.length > 0;
|
|
372
|
+
}
|
|
335
373
|
/**
|
|
336
374
|
* Assemble the full CMIS statement from the search state. Each unit — the
|
|
337
375
|
* full-text clause plus every type block (type restriction `AND`-ed with its
|
|
@@ -341,11 +379,14 @@ function buildFulltextClause(fulltext) {
|
|
|
341
379
|
* @param blocks The type blocks with their conditions.
|
|
342
380
|
* @param combinator How the top-level units combine (`AND` / `OR`).
|
|
343
381
|
* @param fulltext Optional whole-object full-text unit.
|
|
382
|
+
* @param scopeTypes The types the whole search is scoped to (the host's `types` allow-list).
|
|
383
|
+
* Applied to the full-text unit when it targets "all types"; the blocks always carry their own
|
|
384
|
+
* (already scoped) types. Empty = unscoped, i.e. "all types" restricts nothing.
|
|
344
385
|
* @returns A `SELECT * FROM system:object WHERE …` statement, or `''`.
|
|
345
386
|
*/
|
|
346
|
-
function buildCmisQuery(blocks, combinator, fulltext) {
|
|
387
|
+
function buildCmisQuery(blocks, combinator, fulltext, scopeTypes = []) {
|
|
347
388
|
const units = [];
|
|
348
|
-
const fulltextClause = fulltext ? buildFulltextClause(fulltext) : '';
|
|
389
|
+
const fulltextClause = fulltext ? buildFulltextClause(fulltext, scopeTypes) : '';
|
|
349
390
|
if (fulltextClause !== '')
|
|
350
391
|
units.push(fulltextClause);
|
|
351
392
|
for (const block of blocks) {
|
|
@@ -503,11 +544,15 @@ _('yuv.smart-search.date-preset.this-year');
|
|
|
503
544
|
_('yuv.smart-search.operator.like');
|
|
504
545
|
_('yuv.smart-search.operator.empty');
|
|
505
546
|
_('yuv.smart-search.operator.not-empty');
|
|
547
|
+
_('yuv.smart-search.operator.eq-true');
|
|
548
|
+
_('yuv.smart-search.operator.eq-false');
|
|
506
549
|
/* eslint-disable id-length */
|
|
507
550
|
const OPERATOR_LABEL_KEYS = {
|
|
508
551
|
like: 'yuv.smart-search.operator.like',
|
|
509
552
|
empty: 'yuv.smart-search.operator.empty',
|
|
510
|
-
not_empty: 'yuv.smart-search.operator.not-empty'
|
|
553
|
+
not_empty: 'yuv.smart-search.operator.not-empty',
|
|
554
|
+
eq_true: 'yuv.smart-search.operator.eq-true',
|
|
555
|
+
eq_false: 'yuv.smart-search.operator.eq-false'
|
|
511
556
|
};
|
|
512
557
|
const OPERATOR_SYMBOLS = {
|
|
513
558
|
eq: '=',
|
|
@@ -518,12 +563,24 @@ const OPERATOR_SYMBOLS = {
|
|
|
518
563
|
lte: '<='
|
|
519
564
|
};
|
|
520
565
|
/* eslint-enable id-length */
|
|
566
|
+
/** Value equality for a normalized condition value (a string or a list of strings). */
|
|
567
|
+
function sameConditionValue(left, right) {
|
|
568
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
569
|
+
return (Array.isArray(left) &&
|
|
570
|
+
Array.isArray(right) &&
|
|
571
|
+
left.length === right.length &&
|
|
572
|
+
left.every((entry, index) => entry === right[index]));
|
|
573
|
+
}
|
|
574
|
+
return left === right;
|
|
575
|
+
}
|
|
521
576
|
const DATE_PRESETS = [
|
|
522
577
|
{ id: 'today', labelKey: 'yuv.smart-search.date-preset.today' },
|
|
523
578
|
{ id: 'thisWeek', labelKey: 'yuv.smart-search.date-preset.this-week' },
|
|
524
579
|
{ id: 'thisMonth', labelKey: 'yuv.smart-search.date-preset.this-month' },
|
|
525
580
|
{ id: 'thisYear', labelKey: 'yuv.smart-search.date-preset.this-year' }
|
|
526
581
|
];
|
|
582
|
+
/** Operator id (`date:<preset>`) → translation key, so a committed preset can be re-labelled. */
|
|
583
|
+
const DATE_PRESET_LABEL_KEYS = Object.fromEntries(DATE_PRESETS.map((preset) => [`date:${preset.id}`, preset.labelKey]));
|
|
527
584
|
/**
|
|
528
585
|
* State + mutators for SmartSearch, shared between the host component and the
|
|
529
586
|
* recursive `SmartSearchGroupComponent`. Provided at the host component level
|
|
@@ -536,7 +593,10 @@ class SmartSearchEditController {
|
|
|
536
593
|
constructor() {
|
|
537
594
|
this.#system = inject(SystemService);
|
|
538
595
|
this.#translate = inject(TranslateService);
|
|
539
|
-
/**
|
|
596
|
+
/**
|
|
597
|
+
* The set of object type IDs the search is scoped to. Empty = unscoped: every
|
|
598
|
+
* searchable type of the schema is a candidate and nothing is restricted.
|
|
599
|
+
*/
|
|
540
600
|
this.allowedTypes = signal([], ...(ngDevMode ? [{ debugName: "allowedTypes" }] : /* istanbul ignore next */ []));
|
|
541
601
|
/** ObjectTypeField IDs to exclude from the field-step autocomplete suggestions. */
|
|
542
602
|
this.skipProperties = signal([], ...(ngDevMode ? [{ debugName: "skipProperties" }] : /* istanbul ignore next */ []));
|
|
@@ -579,6 +639,12 @@ class SmartSearchEditController {
|
|
|
579
639
|
this.fulltext = signal({ term: '', scope: 'all', types: [] }, ...(ngDevMode ? [{ debugName: "fulltext" }] : /* istanbul ignore next */ []));
|
|
580
640
|
/** Whether the dynamic-conditions feature is enabled (mirrors the host `supportDynamicConditions` input). */
|
|
581
641
|
this.supportDynamic = signal(false, ...(ngDevMode ? [{ debugName: "supportDynamic" }] : /* istanbul ignore next */ []));
|
|
642
|
+
/**
|
|
643
|
+
* Whether the host renders a plain full-text search (mirrors its `fulltextOnly` input).
|
|
644
|
+
* The condition builder is hidden and {@link cmisQuery} is built from {@link fulltext}
|
|
645
|
+
* alone, so blocks a loaded state carries can't contribute invisible clauses.
|
|
646
|
+
*/
|
|
647
|
+
this.fulltextOnly = signal(false, ...(ngDevMode ? [{ debugName: "fulltextOnly" }] : /* istanbul ignore next */ []));
|
|
582
648
|
/**
|
|
583
649
|
* Form mode: replace the builder with a generated form of the user-marked dynamic
|
|
584
650
|
* conditions. View state only — the template ({@link blocks}) is never mutated; the
|
|
@@ -598,7 +664,35 @@ class SmartSearchEditController {
|
|
|
598
664
|
this.#formValues = signal(new Map(), ...(ngDevMode ? [{ debugName: "#formValues" }] : /* istanbul ignore next */ []));
|
|
599
665
|
/** Whether any condition is marked dynamic (gates the form-mode toggle). */
|
|
600
666
|
this.hasDynamicConditions = computed(() => this.blocks().some((block) => this.#anyDynamic(block.conditions)), ...(ngDevMode ? [{ debugName: "hasDynamicConditions" }] : /* istanbul ignore next */ []));
|
|
601
|
-
|
|
667
|
+
// Alphabetical by label, collated in the active UI language. Types without a localized label
|
|
668
|
+
// fall back to their id, normalized here so the sort key matches what consumers render.
|
|
669
|
+
// Consumed by the full-text types multi-select in the template and, via `suggestions()`, by
|
|
670
|
+
// the block-type autocomplete; this is the single ordering authority for both, so neither
|
|
671
|
+
// consumer re-sorts. The "All types" sentinel isn't part of this list; it's a separate,
|
|
672
|
+
// always-first option the consuming template renders ahead of these.
|
|
673
|
+
this.objectTypes = computed(() => {
|
|
674
|
+
// Plain method call, not a signal: a live language switch does not re-run this computed.
|
|
675
|
+
const lang = this.#translate.getCurrentLang();
|
|
676
|
+
return this.#system
|
|
677
|
+
.getObjectTypes(true, 'search')
|
|
678
|
+
.filter((type) => this.allowedTypes().includes(type.id))
|
|
679
|
+
.map((type) => ({ ...type, label: type.label || type.id }))
|
|
680
|
+
.sort((a, b) => a.label.localeCompare(b.label, lang));
|
|
681
|
+
}, ...(ngDevMode ? [{ debugName: "objectTypes" }] : /* istanbul ignore next */ []));
|
|
682
|
+
/**
|
|
683
|
+
* The types an "all types" pick resolves to. With a configured allow-list, "all types"
|
|
684
|
+
* means *any of the allowed types* — not every type of the system — so the restriction
|
|
685
|
+
* is still emitted. Without one the list is empty and "all types" restricts nothing.
|
|
686
|
+
*
|
|
687
|
+
* Resolved id-by-id against the schema (rather than from {@link objectTypes}) so an
|
|
688
|
+
* allowed id the current schema doesn't know still narrows the query instead of silently
|
|
689
|
+
* widening it; `isSot` decides whether the id lands in the primary or the secondary
|
|
690
|
+
* object-type clause.
|
|
691
|
+
*/
|
|
692
|
+
this.scopeTypes = computed(() => this.allowedTypes().map((id) => {
|
|
693
|
+
const known = this.#system.getObjectType(id, true);
|
|
694
|
+
return { id, label: known?.label ?? id, isSot: known?.isSot };
|
|
695
|
+
}), ...(ngDevMode ? [{ debugName: "scopeTypes" }] : /* istanbul ignore next */ []));
|
|
602
696
|
/** Fields for the active top-level block, used by the field-step suggestions. */
|
|
603
697
|
this.activeBlockFields = computed(() => this.#sharedFields(this.activeBlock()?.types ?? []), ...(ngDevMode ? [{ debugName: "activeBlockFields" }] : /* istanbul ignore next */ []));
|
|
604
698
|
/**
|
|
@@ -627,10 +721,10 @@ class SmartSearchEditController {
|
|
|
627
721
|
// Only hide types already staged in the *current* draft block. Types used by
|
|
628
722
|
// other blocks stay available — a new block may target the same type again.
|
|
629
723
|
const stagedIds = new Set(this.draftTypes().map((type) => type.id));
|
|
724
|
+
// `filter`/`map` preserve order, so these arrive already alphabetical from `objectTypes()`.
|
|
630
725
|
const all = this.objectTypes()
|
|
631
726
|
.filter((type) => !stagedIds.has(type.id))
|
|
632
|
-
.map((type) => ({ kind: 'type', id: type.id, label: type.label ?? type.id }))
|
|
633
|
-
.sort((a, b) => a.label.localeCompare(b.label));
|
|
727
|
+
.map((type) => ({ kind: 'type', id: type.id, label: type.label ?? type.id }));
|
|
634
728
|
return term ? all.filter((item) => item.label.toLowerCase().includes(term)) : all;
|
|
635
729
|
}
|
|
636
730
|
if (step === 'field') {
|
|
@@ -648,14 +742,23 @@ class SmartSearchEditController {
|
|
|
648
742
|
}
|
|
649
743
|
return [];
|
|
650
744
|
}, ...(ngDevMode ? [{ debugName: "suggestions" }] : /* istanbul ignore next */ []));
|
|
651
|
-
/**
|
|
652
|
-
|
|
745
|
+
/**
|
|
746
|
+
* Whether the full-text unit contributes to the query — a term, a type restriction, or both.
|
|
747
|
+
* Drives its own muted styling and the joiner below it, so what reads as active matches what
|
|
748
|
+
* is actually queried (see {@link isFulltextActive}).
|
|
749
|
+
*/
|
|
750
|
+
this.fulltextActive = computed(() => isFulltextActive(this.fulltext()), ...(ngDevMode ? [{ debugName: "fulltextActive" }] : /* istanbul ignore next */ []));
|
|
751
|
+
/** Number of top-level query units: the full-text unit (when it contributes one) plus each block. */
|
|
752
|
+
this.unitCount = computed(() => (this.fulltextActive() ? 1 : 0) + this.blocks().length, ...(ngDevMode ? [{ debugName: "unitCount" }] : /* istanbul ignore next */ []));
|
|
653
753
|
this.showCombinator = computed(() => this.unitCount() >= 2, ...(ngDevMode ? [{ debugName: "showCombinator" }] : /* istanbul ignore next */ []));
|
|
654
754
|
this.cmisQuery = computed(() => {
|
|
755
|
+
// Full-text only: the blocks are not rendered, so they must not be queried either.
|
|
756
|
+
if (this.fulltextOnly())
|
|
757
|
+
return buildCmisQuery([], this.combinator(), this.fulltext(), this.scopeTypes());
|
|
655
758
|
// In form mode, overlay the form's entered values onto the (untouched) template so
|
|
656
759
|
// the emitted query reflects the user's fill-out without mutating the saved blocks.
|
|
657
760
|
const blocks = this.formMode() ? overlayConditionValues(this.blocks(), this.#formValues()) : this.blocks();
|
|
658
|
-
return buildCmisQuery(blocks, this.combinator(), this.fulltext());
|
|
761
|
+
return buildCmisQuery(blocks, this.combinator(), this.fulltext(), this.scopeTypes());
|
|
659
762
|
}, ...(ngDevMode ? [{ debugName: "cmisQuery" }] : /* istanbul ignore next */ []));
|
|
660
763
|
/** The active input control for the current step. */
|
|
661
764
|
this.activeCtrl = computed(() => {
|
|
@@ -688,7 +791,16 @@ class SmartSearchEditController {
|
|
|
688
791
|
fulltext: JSON.parse(JSON.stringify(this.fulltext()))
|
|
689
792
|
};
|
|
690
793
|
}
|
|
691
|
-
/**
|
|
794
|
+
/**
|
|
795
|
+
* Replace the current state with a saved one, normalizing legacy blocks and cancelling any
|
|
796
|
+
* in-progress edit.
|
|
797
|
+
*
|
|
798
|
+
* Everything a persisted state carries for display only — type, field and operator labels and
|
|
799
|
+
* the composed condition label — is re-resolved against the live schema and the *current* UI
|
|
800
|
+
* language (see {@link #resolveType} / {@link #relabelNodes}), so a query saved in one language
|
|
801
|
+
* reads in the language it is restored in. The persisted strings act as the fallback for ids the
|
|
802
|
+
* schema or the translations can no longer resolve.
|
|
803
|
+
*/
|
|
692
804
|
loadState(state) {
|
|
693
805
|
this.cancelPending();
|
|
694
806
|
// Form-mode overlay keys reference the old tree; drop form mode on reload.
|
|
@@ -696,7 +808,7 @@ class SmartSearchEditController {
|
|
|
696
808
|
this.blocks.set(state.blocks.map((block) => this.#normalizeBlock(block)));
|
|
697
809
|
this.combinator.set(state.combinator);
|
|
698
810
|
const fulltext = state.fulltext ?? { term: '', scope: 'all', types: [] };
|
|
699
|
-
this.fulltext.set({ ...fulltext, types: fulltext.types.map((type) => this.#
|
|
811
|
+
this.fulltext.set({ ...fulltext, types: fulltext.types.map((type) => this.#resolveType(type)) });
|
|
700
812
|
}
|
|
701
813
|
/** Reset all search state back to its initial empty values. */
|
|
702
814
|
reset() {
|
|
@@ -764,9 +876,27 @@ class SmartSearchEditController {
|
|
|
764
876
|
else
|
|
765
877
|
this.enterFormMode();
|
|
766
878
|
}
|
|
767
|
-
/**
|
|
879
|
+
/**
|
|
880
|
+
* Record a value entered in the form for a dynamic condition (drives the overlaid
|
|
881
|
+
* {@link cmisQuery}). No-op when unchanged, so re-applying a row's current value —
|
|
882
|
+
* e.g. flushing the debounced controls on submit — can't churn the query.
|
|
883
|
+
*/
|
|
768
884
|
setFormValue(condition, raw) {
|
|
769
|
-
|
|
885
|
+
const next = normalizeConditionValue(raw);
|
|
886
|
+
const current = this.#formValues().get(condition);
|
|
887
|
+
if (current !== undefined && sameConditionValue(current, next))
|
|
888
|
+
return;
|
|
889
|
+
this.#formValues.update((values) => new Map(values).set(condition, next));
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Re-apply every generated form row's current control value. The row controls are
|
|
893
|
+
* debounced, so a submit fired immediately after typing would otherwise read a stale
|
|
894
|
+
* overlay. Rows whose value already matches are skipped by {@link setFormValue}.
|
|
895
|
+
*/
|
|
896
|
+
flushFormValues() {
|
|
897
|
+
for (const field of this.formFields()) {
|
|
898
|
+
this.setFormValue(field.condition, field.control.value);
|
|
899
|
+
}
|
|
770
900
|
}
|
|
771
901
|
/** Whether any condition in `nodes` (recursively) is marked dynamic and has a fillable value. */
|
|
772
902
|
#anyDynamic(nodes) {
|
|
@@ -782,8 +912,10 @@ class SmartSearchEditController {
|
|
|
782
912
|
return this.resolveFieldDefinition(block.types, fieldId);
|
|
783
913
|
}
|
|
784
914
|
// ── Full-text unit ────────────────────────────────────────────────────────
|
|
785
|
-
/** Set the full-text search term. */
|
|
915
|
+
/** Set the full-text search term. No-op when unchanged, so re-setting it can't churn {@link cmisQuery}. */
|
|
786
916
|
setFulltextTerm(term) {
|
|
917
|
+
if (this.fulltext().term === term)
|
|
918
|
+
return;
|
|
787
919
|
this.fulltext.update((current) => ({ ...current, term }));
|
|
788
920
|
}
|
|
789
921
|
/** Set the full-text search scope (`all` / `metadata` / `content`). */
|
|
@@ -957,7 +1089,12 @@ class SmartSearchEditController {
|
|
|
957
1089
|
this.pendingField.set(fieldItem);
|
|
958
1090
|
this.pendingOperatorLabel.set(this.operatorLabel(condition.operator));
|
|
959
1091
|
this.fieldCtrl.setValue(condition.fieldLabel, { emitEvent: false });
|
|
960
|
-
|
|
1092
|
+
// The operator is shown as a pill (from pendingOperatorLabel), so its control stays
|
|
1093
|
+
// empty: it is the filter box for picking a *replacement*, not a text rendition of
|
|
1094
|
+
// the current operator. Prefilling it would both make the label look editable and
|
|
1095
|
+
// filter the operator suggestions down to itself.
|
|
1096
|
+
this.operatorCtrl.setValue('', { emitEvent: false });
|
|
1097
|
+
this.inputTerm.set('');
|
|
961
1098
|
this.step.set(isValuelessOperator(condition.operator) ? 'operator' : 'value');
|
|
962
1099
|
}
|
|
963
1100
|
/** Jump back to the field step and clear the input so the user can re-type. */
|
|
@@ -1054,14 +1191,25 @@ class SmartSearchEditController {
|
|
|
1054
1191
|
operatorsForField(field) {
|
|
1055
1192
|
return this.#operatorsForInternalType(field.internalType ?? 'string');
|
|
1056
1193
|
}
|
|
1057
|
-
/**
|
|
1194
|
+
/**
|
|
1195
|
+
* Human-readable label for an operator id: a math symbol (`=`, `≠`, …), a translated
|
|
1196
|
+
* key, a translated date-preset label (`date:thisMonth` → “This month”) or the id itself.
|
|
1197
|
+
*/
|
|
1058
1198
|
operatorLabel(operator) {
|
|
1059
1199
|
const symbol = OPERATOR_SYMBOLS[operator];
|
|
1060
1200
|
if (symbol)
|
|
1061
1201
|
return symbol;
|
|
1062
|
-
const key = OPERATOR_LABEL_KEYS[operator];
|
|
1202
|
+
const key = OPERATOR_LABEL_KEYS[operator] ?? DATE_PRESET_LABEL_KEYS[operator];
|
|
1063
1203
|
return key ? this.#translate.instant(key) : operator;
|
|
1064
1204
|
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Whether {@link operatorLabel} can produce a real label for an operator id, i.e. whether it is
|
|
1207
|
+
* one the builder offers. `false` for anything externally authored, where `operatorLabel` falls
|
|
1208
|
+
* back to echoing the id.
|
|
1209
|
+
*/
|
|
1210
|
+
#isKnownOperator(operator) {
|
|
1211
|
+
return operator in OPERATOR_SYMBOLS || operator in OPERATOR_LABEL_KEYS || operator in DATE_PRESET_LABEL_KEYS;
|
|
1212
|
+
}
|
|
1065
1213
|
/** Build a condition record from a date preset / boolean operator / value. */
|
|
1066
1214
|
buildCommitCondition(field, operatorId, operatorLabelText, value, internalTypeOverride) {
|
|
1067
1215
|
if (operatorId.startsWith('date:')) {
|
|
@@ -1072,7 +1220,7 @@ class SmartSearchEditController {
|
|
|
1072
1220
|
operator: operatorId,
|
|
1073
1221
|
operatorLabel: operatorLabelText,
|
|
1074
1222
|
value: operatorId.slice(DATE_PREFIX_LEN$1),
|
|
1075
|
-
conditionLabel:
|
|
1223
|
+
conditionLabel: composeConditionLabel(field.label, operatorId, operatorLabelText, '')
|
|
1076
1224
|
};
|
|
1077
1225
|
}
|
|
1078
1226
|
if (operatorId === 'empty' || operatorId === 'not_empty') {
|
|
@@ -1083,7 +1231,7 @@ class SmartSearchEditController {
|
|
|
1083
1231
|
operator: operatorId,
|
|
1084
1232
|
operatorLabel: operatorLabelText,
|
|
1085
1233
|
value: '',
|
|
1086
|
-
conditionLabel:
|
|
1234
|
+
conditionLabel: composeConditionLabel(field.label, operatorId, operatorLabelText, '')
|
|
1087
1235
|
};
|
|
1088
1236
|
}
|
|
1089
1237
|
if (operatorId === 'eq_true' || operatorId === 'eq_false') {
|
|
@@ -1095,10 +1243,9 @@ class SmartSearchEditController {
|
|
|
1095
1243
|
operator: 'eq',
|
|
1096
1244
|
operatorLabel: '=',
|
|
1097
1245
|
value: val,
|
|
1098
|
-
conditionLabel:
|
|
1246
|
+
conditionLabel: composeConditionLabel(field.label, 'eq', '=', val)
|
|
1099
1247
|
};
|
|
1100
1248
|
}
|
|
1101
|
-
const valueLabel = Array.isArray(value) ? value.join(', ') : value;
|
|
1102
1249
|
return {
|
|
1103
1250
|
fieldId: field.id,
|
|
1104
1251
|
internalType: internalTypeOverride ?? field.internalType ?? 'string',
|
|
@@ -1106,11 +1253,7 @@ class SmartSearchEditController {
|
|
|
1106
1253
|
operator: operatorId,
|
|
1107
1254
|
operatorLabel: operatorLabelText,
|
|
1108
1255
|
value,
|
|
1109
|
-
|
|
1110
|
-
// cleanly ("Name like") instead of carrying a trailing space.
|
|
1111
|
-
conditionLabel: valueLabel
|
|
1112
|
-
? `${field.label} ${operatorLabelText} ${valueLabel}`
|
|
1113
|
-
: `${field.label} ${operatorLabelText}`
|
|
1256
|
+
conditionLabel: composeConditionLabel(field.label, operatorId, operatorLabelText, value)
|
|
1114
1257
|
};
|
|
1115
1258
|
}
|
|
1116
1259
|
/**
|
|
@@ -1165,9 +1308,9 @@ class SmartSearchEditController {
|
|
|
1165
1308
|
this.#blockSeq = Math.max(this.#blockSeq, Number(match[1]));
|
|
1166
1309
|
}
|
|
1167
1310
|
/**
|
|
1168
|
-
* Normalize a block coming from a loaded state. Ensures an `id`,
|
|
1169
|
-
*
|
|
1170
|
-
*
|
|
1311
|
+
* Normalize a block coming from a loaded state. Ensures an `id`, migrates legacy
|
|
1312
|
+
* single-type blocks (`typeId`/`typeLabel`/`isSot`) into the `types[]` shape so older
|
|
1313
|
+
* saved states remain loadable, and re-resolves every display label the state carried.
|
|
1171
1314
|
*/
|
|
1172
1315
|
#normalizeBlock(block) {
|
|
1173
1316
|
const legacy = block;
|
|
@@ -1178,23 +1321,72 @@ class SmartSearchEditController {
|
|
|
1178
1321
|
this.#trackSeq(id);
|
|
1179
1322
|
return {
|
|
1180
1323
|
id,
|
|
1181
|
-
types: types.map((type) => this.#
|
|
1182
|
-
conditions: legacy.conditions ?? [],
|
|
1324
|
+
types: types.map((type) => this.#resolveType(type)),
|
|
1325
|
+
conditions: this.#relabelNodes(legacy.conditions ?? []),
|
|
1183
1326
|
conditionCombinator: legacy.conditionCombinator ?? 'AND'
|
|
1184
1327
|
};
|
|
1185
1328
|
}
|
|
1186
1329
|
/**
|
|
1187
|
-
* Re-derive a type's `isSot` flag from the live schema.
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
1191
|
-
*
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
1330
|
+
* Re-derive a type's display label and its `isSot` flag from the live schema.
|
|
1331
|
+
*
|
|
1332
|
+
* `isSot` drives whether the type restriction is emitted as `objectTypeId = …` or
|
|
1333
|
+
* `system:secondaryObjectTypeIds IN (…)`, so it must reflect the schema's current
|
|
1334
|
+
* classification rather than whatever was carried in by a persisted state (legacy saved
|
|
1335
|
+
* queries predate the flag, and externally-constructed types may omit it). Resolved via the
|
|
1336
|
+
* schema (not the `allowedTypes`-filtered list) so a saved type outside the current allow-list
|
|
1337
|
+
* is still corrected; unknown ids keep their flag untouched.
|
|
1338
|
+
*
|
|
1339
|
+
* The label is presentation only, but a persisted one freezes the language the state was saved
|
|
1340
|
+
* in — so it is re-resolved the same way the type suggestions are built, falling back to the
|
|
1341
|
+
* persisted label for an id the current translations don't cover.
|
|
1194
1342
|
*/
|
|
1195
|
-
#
|
|
1343
|
+
#resolveType(type) {
|
|
1344
|
+
const resolved = { ...type, label: this.#resolveLabel(type.id, type.label) };
|
|
1196
1345
|
const known = this.#system.getObjectType(type.id);
|
|
1197
|
-
return known ? { ...
|
|
1346
|
+
return known ? { ...resolved, isSot: known.isSot } : resolved;
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Localized label for a schema id (object type, field or table column), falling back to the
|
|
1350
|
+
* label a persisted state carried and finally to the bare id. Mirrors the resolution the
|
|
1351
|
+
* suggestion lists use ({@link #sharedFields} / {@link #columnSuggestions}), so a restored chip
|
|
1352
|
+
* reads like a freshly built one.
|
|
1353
|
+
*/
|
|
1354
|
+
#resolveLabel(id, persisted) {
|
|
1355
|
+
return this.#system.getLocalizedLabel(id) || persisted || id;
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Re-resolve the display labels of a condition tree coming from a loaded state, preserving its
|
|
1359
|
+
* structure and everything that drives the query (`fieldId`, `operator`, `value`, `dynamic`).
|
|
1360
|
+
* Recurses through groups and table conditions.
|
|
1361
|
+
*/
|
|
1362
|
+
#relabelNodes(nodes) {
|
|
1363
|
+
return nodes.map((node) => {
|
|
1364
|
+
if (isTableCondition(node)) {
|
|
1365
|
+
return {
|
|
1366
|
+
...node,
|
|
1367
|
+
fieldLabel: this.#resolveLabel(node.fieldId, node.fieldLabel),
|
|
1368
|
+
conditions: this.#relabelNodes(node.conditions)
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
if (isConditionGroup(node))
|
|
1372
|
+
return { ...node, conditions: this.#relabelNodes(node.conditions) };
|
|
1373
|
+
return this.#relabelCondition(node);
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
/** Re-resolve a single condition's field / operator / chip labels (see {@link #relabelNodes}). */
|
|
1377
|
+
#relabelCondition(condition) {
|
|
1378
|
+
const fieldLabel = this.#resolveLabel(condition.fieldId, condition.fieldLabel);
|
|
1379
|
+
// For an operator the builder doesn't know, operatorLabel() would echo the raw id — keep the
|
|
1380
|
+
// persisted label instead.
|
|
1381
|
+
const operatorLabel = this.#isKnownOperator(condition.operator)
|
|
1382
|
+
? this.operatorLabel(condition.operator)
|
|
1383
|
+
: condition.operatorLabel || condition.operator;
|
|
1384
|
+
return {
|
|
1385
|
+
...condition,
|
|
1386
|
+
fieldLabel,
|
|
1387
|
+
operatorLabel,
|
|
1388
|
+
conditionLabel: composeConditionLabel(fieldLabel, condition.operator, operatorLabel, condition.value)
|
|
1389
|
+
};
|
|
1198
1390
|
}
|
|
1199
1391
|
/**
|
|
1200
1392
|
* Fields shared by *all* given types (the intersection by field id), shaped as
|
|
@@ -1318,11 +1510,7 @@ class SmartSearchEditController {
|
|
|
1318
1510
|
];
|
|
1319
1511
|
case 'boolean':
|
|
1320
1512
|
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
|
-
];
|
|
1513
|
+
return [...base(['eq_true', 'eq_false']), ...empty()];
|
|
1326
1514
|
case 'string:catalog':
|
|
1327
1515
|
case 'string:catalog:i18n':
|
|
1328
1516
|
case 'string:catalog:dynamic':
|
|
@@ -1453,6 +1641,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
1453
1641
|
|
|
1454
1642
|
const DEBOUNCE_MS = 150;
|
|
1455
1643
|
const DATE_PREFIX_LEN = 5;
|
|
1644
|
+
/** `keyCode` browsers report while an IME composition session is active. */
|
|
1645
|
+
const IME_KEY_CODE = 229;
|
|
1456
1646
|
/**
|
|
1457
1647
|
* Visual query builder that turns guided, chip-based user input into a CMIS query.
|
|
1458
1648
|
*
|
|
@@ -1462,15 +1652,26 @@ const DATE_PREFIX_LEN = 5;
|
|
|
1462
1652
|
* - one or more **type blocks**, each targeting one or more object types and holding
|
|
1463
1653
|
* field conditions, nested groups and table-column conditions.
|
|
1464
1654
|
*
|
|
1655
|
+
* Set {@link fulltextOnly} to drop the second part entirely and render a plain full-text
|
|
1656
|
+
* search bar.
|
|
1657
|
+
*
|
|
1465
1658
|
* Conditions are built step by step (type → field → operator → value) with an inline
|
|
1466
1659
|
* autocomplete editor; the value step renders the field's real metadata widget
|
|
1467
1660
|
* (datepicker, catalog select, organization picker, …). The resulting CMIS query is
|
|
1468
1661
|
* emitted through {@link queryChange} on every change and can be saved/restored as a
|
|
1469
|
-
* plain-data {@link SmartSearchState} via {@link getState} / {@link loadState}.
|
|
1662
|
+
* plain-data {@link SmartSearchState} via {@link getState} / {@link loadState}. A restored
|
|
1663
|
+
* state is re-labelled in the current UI language (see {@link loadState}).
|
|
1470
1664
|
*
|
|
1471
1665
|
* State and mutators live in {@link SmartSearchEditController} (provided per instance);
|
|
1472
1666
|
* this component owns only the UI concerns (focus, autocomplete plumbing, blur handling).
|
|
1473
1667
|
*
|
|
1668
|
+
* **Submitting.** {@link queryChange} is a live preview — it fires on every change. To run
|
|
1669
|
+
* a search only when the user is done, bind {@link querySubmit}: it emits when ENTER
|
|
1670
|
+
* reaches the component without an inner widget having claimed it, the way a plain HTML
|
|
1671
|
+
* form submits on ENTER. Widgets that own ENTER for their own purpose keep it: the
|
|
1672
|
+
* suggestion autocomplete picks an option, a catalog select picks a value, and the inline
|
|
1673
|
+
* editor commits the condition it is building. Only the *next* ENTER then submits.
|
|
1674
|
+
*
|
|
1474
1675
|
* @example
|
|
1475
1676
|
* ```html
|
|
1476
1677
|
* <!-- Restrict the picker to two object types and skip a noisy property -->
|
|
@@ -1509,9 +1710,14 @@ class SmartSearchComponent {
|
|
|
1509
1710
|
this.#host = inject((ElementRef));
|
|
1510
1711
|
this.ctrl = inject(SmartSearchEditController);
|
|
1511
1712
|
/**
|
|
1512
|
-
* Object-type ids that may be searched
|
|
1513
|
-
* full-text type filter and the type-block multi-select)
|
|
1514
|
-
*
|
|
1713
|
+
* Object-type ids that may be searched — the scope of the whole search. Restricts
|
|
1714
|
+
* the type picker (both the full-text type filter and the type-block multi-select)
|
|
1715
|
+
* to these types, and narrows the emitted query: picking *All types* in the
|
|
1716
|
+
* full-text filter then means "any of these types" (`objectTypeId IN (…)`), not
|
|
1717
|
+
* every type the system knows.
|
|
1718
|
+
*
|
|
1719
|
+
* When empty the search is unscoped — *All types* restricts nothing — and no type
|
|
1720
|
+
* is offered in the picker, so only a full-text search can be built.
|
|
1515
1721
|
*/
|
|
1516
1722
|
this.types = input([], ...(ngDevMode ? [{ debugName: "types" }] : /* istanbul ignore next */ []));
|
|
1517
1723
|
/**
|
|
@@ -1520,6 +1726,14 @@ class SmartSearchComponent {
|
|
|
1520
1726
|
* table columns alike.
|
|
1521
1727
|
*/
|
|
1522
1728
|
this.skipProperties = input([], ...(ngDevMode ? [{ debugName: "skipProperties" }] : /* istanbul ignore next */ []));
|
|
1729
|
+
/**
|
|
1730
|
+
* Renders the component as a plain full-text search: only the full-text bar (term, scope
|
|
1731
|
+
* and the object-type filter) is shown — no type blocks, no condition builder, no
|
|
1732
|
+
* form-mode toggle. The emitted query is built from the full-text unit alone, so a
|
|
1733
|
+
* previously {@link loadState loaded} state's blocks do not contribute while this is set.
|
|
1734
|
+
* {@link types} keeps its meaning: the search stays scoped to those types.
|
|
1735
|
+
*/
|
|
1736
|
+
this.fulltextOnly = input(false, ...(ngDevMode ? [{ debugName: "fulltextOnly" }] : /* istanbul ignore next */ []));
|
|
1523
1737
|
/**
|
|
1524
1738
|
* Enables the **dynamic conditions** feature. When `true`, each committed condition can
|
|
1525
1739
|
* be marked dynamic and a form-mode toggle appears that swaps the builder for a generated
|
|
@@ -1534,6 +1748,14 @@ class SmartSearchComponent {
|
|
|
1534
1748
|
* non-empty values.
|
|
1535
1749
|
*/
|
|
1536
1750
|
this.queryChange = output();
|
|
1751
|
+
/**
|
|
1752
|
+
* Emits the current CMIS query when the user presses ENTER and no inner widget
|
|
1753
|
+
* claimed that key — the smart-search equivalent of submitting a form. Bind this
|
|
1754
|
+
* (rather than {@link queryChange}) to run the search only once the user is done
|
|
1755
|
+
* building it. Like {@link queryChange}, `''` means "no query"; an empty search
|
|
1756
|
+
* still submits.
|
|
1757
|
+
*/
|
|
1758
|
+
this.querySubmit = output();
|
|
1537
1759
|
this.auto = viewChild.required('auto');
|
|
1538
1760
|
/** Trigger of the currently-focused autocomplete input — used to re-open the
|
|
1539
1761
|
* panel after a type pick so the multi-select stays open. */
|
|
@@ -1542,6 +1764,14 @@ class SmartSearchComponent {
|
|
|
1542
1764
|
this.fulltextTermCtrl = new FormControl('');
|
|
1543
1765
|
/** Sentinel option value representing "no type restriction" in the type multi-select. */
|
|
1544
1766
|
this.ALL_TYPES = '__all__';
|
|
1767
|
+
/**
|
|
1768
|
+
* Empty `matChipInputSeparatorKeyCodes` for the add-type input. The directive defaults
|
|
1769
|
+
* to `[ENTER]` and then calls `preventDefault()` on *every* Enter to end a chip — but
|
|
1770
|
+
* we never bind `matChipInputTokenEnd`, so that only served to hide the key from
|
|
1771
|
+
* {@link onHostEnter}. A stable reference: a `[]` literal in the template would be a
|
|
1772
|
+
* new array on each change-detection run.
|
|
1773
|
+
*/
|
|
1774
|
+
this.NO_SEPARATOR_KEYS = [];
|
|
1545
1775
|
/**
|
|
1546
1776
|
* Guard that prevents `onInlineBlur` from cancelling the pending condition
|
|
1547
1777
|
* when we programmatically open the inline editor.
|
|
@@ -1580,6 +1810,7 @@ class SmartSearchComponent {
|
|
|
1580
1810
|
this.showCombinator = this.ctrl.showCombinator;
|
|
1581
1811
|
this.suggestions = this.ctrl.suggestions;
|
|
1582
1812
|
this.fulltext = this.ctrl.fulltext;
|
|
1813
|
+
this.fulltextActive = this.ctrl.fulltextActive;
|
|
1583
1814
|
this.objectTypes = this.ctrl.objectTypes;
|
|
1584
1815
|
this.activeBlockFields = this.ctrl.activeBlockFields;
|
|
1585
1816
|
this.formMode = this.ctrl.formMode;
|
|
@@ -1630,6 +1861,17 @@ class SmartSearchComponent {
|
|
|
1630
1861
|
effect(() => {
|
|
1631
1862
|
this.ctrl.skipProperties.set(this.skipProperties());
|
|
1632
1863
|
});
|
|
1864
|
+
// Mirror the full-text-only opt-in into the controller (it also drops the blocks from
|
|
1865
|
+
// the emitted query). Turning it on discards an in-progress condition edit and form
|
|
1866
|
+
// mode, so flipping the flag at runtime can't leave hidden UI state active.
|
|
1867
|
+
effect(() => {
|
|
1868
|
+
const only = this.fulltextOnly();
|
|
1869
|
+
this.ctrl.fulltextOnly.set(only);
|
|
1870
|
+
if (only) {
|
|
1871
|
+
this.ctrl.cancelPending();
|
|
1872
|
+
this.ctrl.exitFormMode();
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1633
1875
|
// Mirror the dynamic-conditions opt-in into the controller; leaving the feature
|
|
1634
1876
|
// disabled also forces form mode off.
|
|
1635
1877
|
effect(() => {
|
|
@@ -1708,7 +1950,15 @@ class SmartSearchComponent {
|
|
|
1708
1950
|
getState() {
|
|
1709
1951
|
return this.ctrl.getState();
|
|
1710
1952
|
}
|
|
1711
|
-
/**
|
|
1953
|
+
/**
|
|
1954
|
+
* Restore a previously {@link getState saved} search, replacing the current one.
|
|
1955
|
+
*
|
|
1956
|
+
* The display labels a state carries (type, field and operator labels and the chip's composed
|
|
1957
|
+
* condition label) are re-resolved against the live schema and the *current* UI language, so a
|
|
1958
|
+
* search saved in one language reads in the language it is restored in. Ids the schema or the
|
|
1959
|
+
* translations can no longer resolve keep the label the state carried. Nothing that drives the
|
|
1960
|
+
* emitted query is affected.
|
|
1961
|
+
*/
|
|
1712
1962
|
loadState(state) {
|
|
1713
1963
|
this.ctrl.loadState(state);
|
|
1714
1964
|
// The full-text term input is a local control, not bound to the signal (scope and
|
|
@@ -1882,37 +2132,103 @@ class SmartSearchComponent {
|
|
|
1882
2132
|
* pick, so Enter commits the staged types — even while the autocomplete panel
|
|
1883
2133
|
* is open. While the user is typing a filter term, Enter is left to the
|
|
1884
2134
|
* autocomplete so it can select the highlighted option.
|
|
2135
|
+
*
|
|
2136
|
+
* Unlike {@link onEnter} this deliberately lets an unusable Enter fall through:
|
|
2137
|
+
* the add-type input is the resting state of an empty search, so Enter with
|
|
2138
|
+
* nothing staged has to reach {@link onHostEnter} and submit.
|
|
1885
2139
|
*/
|
|
1886
|
-
onTypeEnter() {
|
|
2140
|
+
onTypeEnter(event) {
|
|
2141
|
+
if (event.defaultPrevented)
|
|
2142
|
+
return;
|
|
1887
2143
|
// The ENTER that selected an autocomplete option must not also confirm the
|
|
1888
2144
|
// block — regardless of whether this handler runs before or after the
|
|
1889
2145
|
// autocomplete closes its panel.
|
|
1890
2146
|
if (this._suppressTypeConfirm)
|
|
1891
2147
|
return;
|
|
2148
|
+
if (this.auto().isOpen)
|
|
2149
|
+
return;
|
|
1892
2150
|
const term = this.ctrl.fieldCtrl.value;
|
|
1893
2151
|
if (typeof term === 'string' && term.trim())
|
|
1894
2152
|
return;
|
|
1895
2153
|
if (this.ctrl.draftTypes().length === 0)
|
|
1896
2154
|
return;
|
|
2155
|
+
// Claim the key so it doesn't also submit the search.
|
|
2156
|
+
event.preventDefault();
|
|
1897
2157
|
this.confirmTypes();
|
|
1898
2158
|
}
|
|
1899
2159
|
/**
|
|
1900
|
-
* Enter / confirm-button handler for the
|
|
1901
|
-
* condition
|
|
1902
|
-
* (Enter selects the highlighted option there)
|
|
2160
|
+
* Enter / confirm-button handler for the inline editor. Commits the in-progress
|
|
2161
|
+
* condition once it has a field and an operator. No-op while the autocomplete
|
|
2162
|
+
* panel is open (Enter selects the highlighted option there).
|
|
2163
|
+
*
|
|
2164
|
+
* Bound on the editor wrapper, so it covers every step — including the value
|
|
2165
|
+
* step's real metadata widget — and runs after the widgets' own key handling.
|
|
2166
|
+
* The `event` is optional because the confirm button calls this from a click.
|
|
1903
2167
|
*/
|
|
1904
|
-
onEnter() {
|
|
2168
|
+
onEnter(event) {
|
|
2169
|
+
if (event?.defaultPrevented)
|
|
2170
|
+
return;
|
|
1905
2171
|
if (this.auto().isOpen)
|
|
1906
2172
|
return;
|
|
1907
|
-
if (this
|
|
2173
|
+
if (event && this.#isMultilineTarget(event))
|
|
1908
2174
|
return;
|
|
1909
|
-
//
|
|
1910
|
-
|
|
2175
|
+
// The inline editor owns Enter while it is open: commit when the condition is
|
|
2176
|
+
// ready, otherwise swallow the key — a half-built condition must not submit.
|
|
2177
|
+
event?.preventDefault();
|
|
1911
2178
|
const field = this.ctrl.pendingField();
|
|
1912
2179
|
const fieldOp = field?.operator ?? '';
|
|
1913
2180
|
if (!field || !fieldOp)
|
|
1914
2181
|
return;
|
|
2182
|
+
// A valueless operator (`is empty`, a date preset) is complete the moment it is
|
|
2183
|
+
// set, and the editor parks on the operator step for it — so confirm has to work
|
|
2184
|
+
// from there too, otherwise re-opening such a chip is a dead end with no way out.
|
|
2185
|
+
const onOperatorStep = this.ctrl.step() === 'operator';
|
|
2186
|
+
if (this.ctrl.step() !== 'value' && !(onOperatorStep && isValuelessOperator(fieldOp)))
|
|
2187
|
+
return;
|
|
2188
|
+
// No value guard: a blank value commits an unset placeholder (fill-in template).
|
|
2189
|
+
// The operator step has no value widget; buildCommitCondition derives the value a
|
|
2190
|
+
// valueless operator carries (the date preset id, or '') from the operator itself.
|
|
2191
|
+
const raw = onOperatorStep ? '' : this.ctrl.valueCtrl.value;
|
|
1915
2192
|
this.ctrl.commitCondition(this.ctrl.buildCommitCondition(field, fieldOp, this.ctrl.operatorLabel(fieldOp), normalizeConditionValue(raw)));
|
|
2193
|
+
// Committing unmounts the editor, which would drop focus to <body>. Hand it to
|
|
2194
|
+
// the add-type input so the next Enter (or the next condition) has a target.
|
|
2195
|
+
setTimeout(() => this.#focusTypeInput());
|
|
2196
|
+
}
|
|
2197
|
+
/**
|
|
2198
|
+
* Enter that bubbled all the way up to the component host without being claimed:
|
|
2199
|
+
* submit the search, the way a plain HTML form does.
|
|
2200
|
+
*
|
|
2201
|
+
* Everything inside that owns Enter marks the event handled via `preventDefault()`
|
|
2202
|
+
* — Material's autocomplete and select do, and so do {@link onEnter} /
|
|
2203
|
+
* {@link onTypeEnter} — so this only sees the "nothing else wanted it" case. The
|
|
2204
|
+
* remaining guards cover keys the browser itself is still acting on.
|
|
2205
|
+
*/
|
|
2206
|
+
onHostEnter(event) {
|
|
2207
|
+
if (event.defaultPrevented)
|
|
2208
|
+
return;
|
|
2209
|
+
// Confirming an IME candidate, not submitting.
|
|
2210
|
+
if (this.#isComposing(event))
|
|
2211
|
+
return;
|
|
2212
|
+
// Enter inserts a newline here; Shift+Enter never reaches us (Angular's
|
|
2213
|
+
// `keydown.enter` matches modifiers exactly).
|
|
2214
|
+
if (this.#isMultilineTarget(event))
|
|
2215
|
+
return;
|
|
2216
|
+
// A native button/link already turned this Enter into a click.
|
|
2217
|
+
if (this.#isActivationTarget(event))
|
|
2218
|
+
return;
|
|
2219
|
+
// A suggestion panel is up but Material left the key untouched (no active option).
|
|
2220
|
+
if (this.auto().isOpen)
|
|
2221
|
+
return;
|
|
2222
|
+
// A raw value renderer mounted its UI in the CDK overlay (mat-select panel,
|
|
2223
|
+
// datepicker dialog) — same reasoning as onInlineBlur.
|
|
2224
|
+
if (document.querySelector('.cdk-overlay-container .cdk-overlay-pane'))
|
|
2225
|
+
return;
|
|
2226
|
+
// The Enter that staged a type can land here after the panel already closed
|
|
2227
|
+
// (matChipInput reorders the keydown listeners — see _suppressTypeConfirm).
|
|
2228
|
+
if (this._suppressTypeConfirm || this._pickerJustSelected)
|
|
2229
|
+
return;
|
|
2230
|
+
this.#flushPendingInput();
|
|
2231
|
+
this.querySubmit.emit(this.ctrl.cmisQuery());
|
|
1916
2232
|
}
|
|
1917
2233
|
/**
|
|
1918
2234
|
* Called when the inline-input wrapper loses focus.
|
|
@@ -1986,8 +2302,13 @@ class SmartSearchComponent {
|
|
|
1986
2302
|
this.ctrl.cancelPending();
|
|
1987
2303
|
});
|
|
1988
2304
|
}
|
|
1989
|
-
/**
|
|
2305
|
+
/**
|
|
2306
|
+
* Abandon the in-progress condition edit, discarding any partial input. When an
|
|
2307
|
+
* existing condition was opened for editing it is put back verbatim rather than
|
|
2308
|
+
* lost — cancelling an edit means "leave it as it was", the same as the blur path.
|
|
2309
|
+
*/
|
|
1990
2310
|
cancelPending() {
|
|
2311
|
+
this.ctrl.restoreEditingSnapshot();
|
|
1991
2312
|
this.ctrl.cancelPending();
|
|
1992
2313
|
}
|
|
1993
2314
|
/** Jump the inline editor back to the field step so the user can re-pick the field. */
|
|
@@ -2094,8 +2415,9 @@ class SmartSearchComponent {
|
|
|
2094
2415
|
}
|
|
2095
2416
|
else {
|
|
2096
2417
|
this.ctrl.pendingField.set({ ...field, operator: item.id });
|
|
2418
|
+
// The pill reads from pendingOperatorLabel; the control is only the filter box.
|
|
2097
2419
|
this.ctrl.pendingOperatorLabel.set(item.label);
|
|
2098
|
-
this.ctrl.operatorCtrl.setValue(
|
|
2420
|
+
this.ctrl.operatorCtrl.setValue('', { emitEvent: false });
|
|
2099
2421
|
this.ctrl.inputTerm.set('');
|
|
2100
2422
|
this.ctrl.step.set('value');
|
|
2101
2423
|
this._suppressNextBlur = true;
|
|
@@ -2115,15 +2437,57 @@ class SmartSearchComponent {
|
|
|
2115
2437
|
const raw = this.ctrl.valueCtrl.value;
|
|
2116
2438
|
this.ctrl.commitCondition(this.ctrl.buildCommitCondition(field, operator, this.ctrl.operatorLabel(operator), normalizeConditionValue(raw)));
|
|
2117
2439
|
}
|
|
2440
|
+
// ── Submit-on-Enter internals (see onHostEnter) ────────────────────────────
|
|
2441
|
+
/**
|
|
2442
|
+
* Apply the debounced inputs (full-text term, fill-out form rows) to the query state
|
|
2443
|
+
* right now. Enter typically arrives well inside the debounce window, so without this
|
|
2444
|
+
* a submit would emit the query as it stood one keystroke ago. Both mutators ignore an
|
|
2445
|
+
* unchanged value, so this is free when nothing is pending.
|
|
2446
|
+
*/
|
|
2447
|
+
#flushPendingInput() {
|
|
2448
|
+
this.ctrl.setFulltextTerm(this.fulltextTermCtrl.value ?? '');
|
|
2449
|
+
if (this.ctrl.formMode())
|
|
2450
|
+
this.ctrl.flushFormValues();
|
|
2451
|
+
}
|
|
2452
|
+
/** Whether the key is confirming an IME candidate rather than acting on its own. */
|
|
2453
|
+
#isComposing(event) {
|
|
2454
|
+
const keyEvent = event;
|
|
2455
|
+
return !!keyEvent.isComposing || keyEvent.keyCode === IME_KEY_CODE;
|
|
2456
|
+
}
|
|
2457
|
+
/** Whether the key landed in a control where Enter inserts a line break. */
|
|
2458
|
+
#isMultilineTarget(event) {
|
|
2459
|
+
const target = event.target;
|
|
2460
|
+
return !!target?.closest?.('textarea, [contenteditable]:not([contenteditable="false"])');
|
|
2461
|
+
}
|
|
2462
|
+
/**
|
|
2463
|
+
* Whether the key landed on an element the browser activates with Enter. Native
|
|
2464
|
+
* buttons fire a click *and* let the keydown bubble, so without this an Enter on
|
|
2465
|
+
* e.g. the block-remove button would also submit the search.
|
|
2466
|
+
*/
|
|
2467
|
+
#isActivationTarget(event) {
|
|
2468
|
+
const target = event.target;
|
|
2469
|
+
return !!target?.closest?.('button, a[href], [role="button"], summary');
|
|
2470
|
+
}
|
|
2118
2471
|
#focusInput() {
|
|
2119
2472
|
this.#host.nativeElement.querySelector('.inline-input input')?.focus();
|
|
2120
2473
|
}
|
|
2474
|
+
/** Focus the add-type input — the editor's resting place once a condition is committed. */
|
|
2475
|
+
#focusTypeInput() {
|
|
2476
|
+
const input = this.#host.nativeElement.querySelector('.add-type-row input');
|
|
2477
|
+
if (!input)
|
|
2478
|
+
return;
|
|
2479
|
+
input.focus();
|
|
2480
|
+
// Focusing an autocomplete input pops its panel open. The user just finished a
|
|
2481
|
+
// condition and didn't ask for type suggestions — and an open panel would swallow
|
|
2482
|
+
// the Enter that submits the search. Typing reopens it.
|
|
2483
|
+
this.trigger()?.closePanel();
|
|
2484
|
+
}
|
|
2121
2485
|
/** Focus the "Add condition" (+) button of a block — used after confirming its types. */
|
|
2122
2486
|
#focusAddCondition(blockId) {
|
|
2123
2487
|
this.#host.nativeElement.querySelector(`.block[data-block-id="${blockId}"] .add-condition-btn`)?.focus();
|
|
2124
2488
|
}
|
|
2125
2489
|
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 }); }
|
|
2490
|
+
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() && !fulltextActive() && 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 (fulltextActive() && 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
2491
|
}
|
|
2128
2492
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SmartSearchComponent, decorators: [{
|
|
2129
2493
|
type: Component,
|
|
@@ -2141,8 +2505,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
2141
2505
|
RendererDirective,
|
|
2142
2506
|
SmartSearchGroupComponent,
|
|
2143
2507
|
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 }] }] } });
|
|
2508
|
+
], 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() && !fulltextActive() && 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 (fulltextActive() && 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"] }]
|
|
2509
|
+
}], 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
2510
|
|
|
2147
2511
|
/**
|
|
2148
2512
|
* Convenience NgModule that imports and re-exports {@link SmartSearchComponent}.
|
|
@@ -2168,5 +2532,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
2168
2532
|
* Generated bundle index. Do not edit.
|
|
2169
2533
|
*/
|
|
2170
2534
|
|
|
2171
|
-
export { SmartSearchComponent, YuvSmartSearchModule, buildCmisQuery, buildFulltextClause, buildNodeClause, isConditionGroup, isFieldCondition, isTableCondition };
|
|
2535
|
+
export { SmartSearchComponent, YuvSmartSearchModule, buildCmisQuery, buildFulltextClause, buildNodeClause, isConditionGroup, isFieldCondition, isFulltextActive, isTableCondition };
|
|
2172
2536
|
//# sourceMappingURL=yuuvis-client-framework-smart-search.mjs.map
|