aloha-vue 2.80.1 → 2.81.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.
@@ -0,0 +1,54 @@
1
+ import {
2
+ nextTick,
3
+ ref,
4
+ } from "vue";
5
+ import {
6
+ describe,
7
+ expect,
8
+ it,
9
+ } from "vitest";
10
+ import {
11
+ shallowMount,
12
+ } from "@vue/test-utils";
13
+
14
+ import AMenu from "../AMenu";
15
+
16
+ describe("AMenu inline placement", () => {
17
+ it("opts into inline placement without changing the default", async() => {
18
+ const wrapper = shallowMount(AMenu, {
19
+ props: {
20
+ canChangeBodyClass: false,
21
+ useBackdropMobile: false,
22
+ },
23
+ global: {
24
+ stubs: {
25
+ AButton: {
26
+ inheritAttrs: false,
27
+ template: "<button />",
28
+ },
29
+ },
30
+ config: {
31
+ globalProperties: {
32
+ $router: {
33
+ currentRoute: ref({ fullPath: "/" }),
34
+ },
35
+ },
36
+ },
37
+ },
38
+ });
39
+ await nextTick();
40
+
41
+ expect(wrapper.vm.searchInDom).toBe(false);
42
+ expect(wrapper.element.tagName).toBe("NAV");
43
+ expect(wrapper.classes()).not.toContain("a_menu_inline");
44
+
45
+ await wrapper.setProps({ isInline: true });
46
+ expect(wrapper.classes()).toContain("a_menu_inline");
47
+ expect(wrapper.classes()).toContain("a_menu_open");
48
+ expect(document.body.classList.contains("a_menu__wrapper_open")).toBe(false);
49
+
50
+ await wrapper.setProps({ isInline: false });
51
+ expect(wrapper.classes()).not.toContain("a_menu_inline");
52
+ wrapper.unmount();
53
+ });
54
+ });
@@ -5,17 +5,28 @@ import {
5
5
  } from "vue";
6
6
 
7
7
  import {
8
+ getElementId,
9
+ } from "../utils/utils";
10
+
11
+ import AKeyBreadcrumbs from "../const/AKeyBreadcrumbs";
12
+ import {
13
+ AKeyId,
8
14
  AKeyLabel,
9
15
  } from "../../const/AKeys";
16
+ import {
17
+ isBrowser,
18
+ } from "../../utils/isBrowser";
10
19
  import {
11
20
  escapeRegExp,
12
21
  forEach,
22
+ map,
13
23
  } from "lodash-es";
14
24
 
15
25
  export default function SearchAPI(props, {
16
26
  dataKeyById = computed(() => ({})),
17
27
  }) {
18
28
  const menuId = toRef(props, "menuId");
29
+ const searchInDom = toRef(props, "searchInDom");
19
30
 
20
31
  const modelSearch = ref("");
21
32
 
@@ -47,15 +58,47 @@ export default function SearchAPI(props, {
47
58
  return escapeRegExp(modelSearch.value);
48
59
  });
49
60
 
61
+ const getDomLabel = id => {
62
+ if (!isBrowser()) {
63
+ return undefined;
64
+ }
65
+ const LINK = document.getElementById(getElementId({
66
+ menuId: menuId.value,
67
+ id,
68
+ suffix: "link",
69
+ }));
70
+ // Read source panels only; search results contain copies of the same links.
71
+ if (!LINK?.closest(".a_menu__panels__main:not(.a_menu__panel_search)")) {
72
+ return undefined;
73
+ }
74
+ return (LINK.querySelector(".a_menu__link__text") || LINK).textContent;
75
+ };
76
+
77
+ // shortcut: DOM translations refresh on query changes; observe mutations if live updates are needed.
50
78
  const itemsWithSearch = computed(() => {
51
79
  const ITEMS = [];
52
80
  if (isSearchActive.value) {
53
81
  const RE = new RegExp(modelSearchEscapeRegExp.value, "gi");
54
82
  forEach(dataKeyById.value, item => {
55
- const ITEM_LABEL = item[AKeyLabel];
56
- if (`${ ITEM_LABEL }`.search(RE) !== -1) {
83
+ const ITEM_LABEL = searchInDom.value ? getDomLabel(item[AKeyId]) : item[AKeyLabel];
84
+ if (searchInDom.value && ITEM_LABEL === undefined) {
85
+ return;
86
+ }
87
+ if (`${ ITEM_LABEL }`.search(RE) === -1) {
88
+ return;
89
+ }
90
+ if (!searchInDom.value) {
57
91
  ITEMS.push(item);
92
+ return;
58
93
  }
94
+ ITEMS.push({
95
+ ...item,
96
+ [AKeyLabel]: ITEM_LABEL,
97
+ [AKeyBreadcrumbs]: map(item[AKeyBreadcrumbs], breadcrumb => ({
98
+ ...breadcrumb,
99
+ label: getDomLabel(breadcrumb.parentId) ?? breadcrumb.label,
100
+ })),
101
+ });
59
102
  });
60
103
  }
61
104
  return ITEMS;
@@ -0,0 +1,108 @@
1
+ import {
2
+ computed,
3
+ reactive,
4
+ } from "vue";
5
+ import {
6
+ afterEach,
7
+ beforeEach,
8
+ describe,
9
+ expect,
10
+ it,
11
+ } from "vitest";
12
+
13
+ import LabelAPI from "../../AMenuPanelLink/compositionAPI/LabelAPI";
14
+ import SearchAPI from "../SearchAPI";
15
+
16
+ import AKeyBreadcrumbs from "../../const/AKeyBreadcrumbs";
17
+ import {
18
+ AKeyId,
19
+ AKeyLabel,
20
+ } from "../../../const/AKeys";
21
+
22
+ describe("AMenu search", () => {
23
+ const ITEMS = {
24
+ forms: { [AKeyId]: "forms", [AKeyLabel]: "Forms" },
25
+ input: {
26
+ [AKeyId]: "input",
27
+ [AKeyLabel]: "Input",
28
+ [AKeyBreadcrumbs]: [{ parentId: "forms", label: "Forms" }],
29
+ to: { name: "input" },
30
+ disabled: true,
31
+ },
32
+ missing: { [AKeyId]: "missing", [AKeyLabel]: "Eingabe missing" },
33
+ };
34
+
35
+ beforeEach(() => {
36
+ document.body.innerHTML = `
37
+ <nav id="test_menu">
38
+ <div class="a_menu__panels__main">
39
+ <a id="test_menu_forms_link"><span class="a_menu__link__text">Formulare</span></a>
40
+ <div style="display: none">
41
+ <a id="test_menu_input_link">
42
+ <span class="a_menu__link__text"><span>Eingabe</span> [DE]</span>
43
+ <span class="a_menu__link__counter">99</span>
44
+ </a>
45
+ </div>
46
+ </div>
47
+ <div class="a_menu__panels__main a_menu__panel_search">
48
+ <a id="test_menu_input_link"><span class="a_menu__link__text">Stale result</span></a>
49
+ <a id="test_menu_missing_link"><span class="a_menu__link__text">Eingabe missing</span></a>
50
+ </div>
51
+ </nav>`;
52
+ });
53
+
54
+ afterEach(() => {
55
+ document.body.innerHTML = "";
56
+ });
57
+
58
+ it("searches data by default even when DOM text differs", () => {
59
+ const search = SearchAPI(reactive({ menuId: "test_menu" }), {
60
+ dataKeyById: computed(() => ITEMS),
61
+ });
62
+ search.updateModelSearch("INPUT");
63
+ expect(search.itemsWithSearch.value).toEqual([ITEMS.input]);
64
+ expect(search.itemsWithSearch.value[0]).toBe(ITEMS.input);
65
+ search.updateModelSearch("Formulare");
66
+ expect(search.itemsWithSearch.value).toEqual([]);
67
+ });
68
+
69
+ it("searches translated text in source panels and preserves link behavior and breadcrumbs", () => {
70
+ const search = SearchAPI(reactive({ menuId: "test_menu", searchInDom: true }), {
71
+ dataKeyById: computed(() => ITEMS),
72
+ });
73
+ search.updateModelSearch("eINGABE");
74
+ expect(search.itemsWithSearch.value).toEqual([{
75
+ ...ITEMS.input,
76
+ [AKeyLabel]: "Eingabe [DE]",
77
+ [AKeyBreadcrumbs]: [{ parentId: "forms", label: "Formulare" }],
78
+ }]);
79
+ const {
80
+ label,
81
+ } = LabelAPI(reactive({
82
+ item: search.itemsWithSearch.value[0],
83
+ isLinkInSearchPanel: true,
84
+ modelSearch: "eINGABE",
85
+ }));
86
+ expect(label.value).toBe("<mark class=\"a_search_highlight\">Eingabe</mark> [DE]");
87
+ expect(ITEMS.input[AKeyLabel]).toBe("Input");
88
+ expect(ITEMS.input[AKeyBreadcrumbs][0].label).toBe("Forms");
89
+ search.updateModelSearch("Input");
90
+ expect(search.itemsWithSearch.value).toEqual([]);
91
+ search.updateModelSearch("99");
92
+ expect(search.itemsWithSearch.value).toEqual([]);
93
+ });
94
+
95
+ it("treats regular expression characters literally and rereads DOM for the next query", () => {
96
+ const search = SearchAPI(reactive({ menuId: "test_menu", searchInDom: true }), {
97
+ dataKeyById: computed(() => ITEMS),
98
+ });
99
+ search.updateModelSearch("[DE]");
100
+ expect(search.itemsWithSearch.value).toHaveLength(1);
101
+ document.querySelector("#test_menu_input_link .a_menu__link__text").textContent = "Saisie (FR)";
102
+ search.updateModelSearch("(FR)");
103
+ expect(search.itemsWithSearch.value[0][AKeyLabel]).toBe("Saisie (FR)");
104
+ search.resetSearch();
105
+ expect(search.isSearchActive.value).toBe(false);
106
+ expect(search.itemsWithSearch.value).toEqual([]);
107
+ });
108
+ });