@simple-reporting/base 1.0.39 → 1.0.40

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.
Files changed (96) hide show
  1. package/dev/package.json +1 -1
  2. package/dev/src/App.vue +1 -0
  3. package/dev/src/assets/scss/editor.scss +1 -0
  4. package/dev/src/assets/scss/general.scss +18 -0
  5. package/dev/src/assets/scss/margins.scss +4 -2
  6. package/dev/src/entries/pdf.ts +5 -0
  7. package/dev/srl.config.json +60 -6
  8. package/livingdocs/010.Titles/010.title-h1/scss/_spacing-variations.scss +0 -2
  9. package/livingdocs/010.Titles/010.title-h1/scss/web.scss +7 -0
  10. package/livingdocs/010.Titles/020.title-h2/scss/_spacing-variations.scss +0 -2
  11. package/livingdocs/010.Titles/020.title-h2/scss/general.scss +10 -0
  12. package/livingdocs/010.Titles/030.title-h3/scss/_spacing-variations.scss +0 -2
  13. package/livingdocs/010.Titles/040.title-h4/scss/_spacing-variations.scss +0 -2
  14. package/livingdocs/010.Titles/050.title-h5/scss/_spacing-variations.scss +0 -2
  15. package/livingdocs/010.Titles/060.title-h6/scss/_spacing-variations.scss +0 -2
  16. package/livingdocs/020.Text/020.paragraph/scss/_spacing-variations.scss +1 -2
  17. package/livingdocs/020.Text/020.paragraph/scss/general.scss +4 -0
  18. package/livingdocs/020.Text/040.link/scss/general.scss +8 -0
  19. package/livingdocs/020.Text/060.quote-with-portrait/scss/_spacing-variations.scss +1 -2
  20. package/livingdocs/020.Text/060.quote-with-portrait/scss/general.scss +12 -0
  21. package/livingdocs/020.Text/060.quote-with-portrait/scss/web.scss +7 -0
  22. package/livingdocs/020.Text/070.footnote-container/scss/_spacing-variations.scss +0 -2
  23. package/livingdocs/020.Text/080.footnote-item/scss/word.scss +8 -0
  24. package/livingdocs/030.Lists/010.unordered-list/scss/_spacing-variations.scss +0 -2
  25. package/livingdocs/040.Media/010.table/scss/_spacing-variations.scss +0 -2
  26. package/livingdocs/040.Media/020.image/scss/_spacing-variations.scss +0 -2
  27. package/livingdocs/040.Media/020.image/scss/web.scss +19 -5
  28. package/livingdocs/040.Media/030.video/ld-conf.json +1 -6
  29. package/livingdocs/040.Media/030.video/properties.json +7 -0
  30. package/livingdocs/040.Media/030.video/scss/_spacing-variations.scss +0 -2
  31. package/livingdocs/040.Media/030.video/scss/editor.scss +10 -0
  32. package/livingdocs/040.Media/030.video/scss/general.scss +6 -1
  33. package/livingdocs/040.Media/030.video/scss/pdf.scss +8 -0
  34. package/livingdocs/040.Media/030.video/video.html +27 -30
  35. package/livingdocs/040.Media/030.video/video.vue +71 -32
  36. package/livingdocs/060.Buttons/020.button/scss/general.scss +2 -2
  37. package/livingdocs/070.Container/010.aside-content-container/scss/pdf.scss +15 -0
  38. package/livingdocs/070.Container/010.aside-content-container/scss/web.scss +4 -0
  39. package/livingdocs/070.Container/020.columns-container/scss/app.scss +1 -0
  40. package/livingdocs/070.Container/020.columns-container/scss/editor.scss +1 -0
  41. package/livingdocs/070.Container/020.columns-container/scss/general.scss +5 -0
  42. package/livingdocs/070.Container/020.columns-container/scss/pdf.scss +8 -0
  43. package/livingdocs/070.Container/020.columns-container/scss/web.scss +1 -0
  44. package/livingdocs/070.Container/020.columns-container/scss/word.scss +1 -0
  45. package/livingdocs/070.Container/020.columns-container/scss/xbrl.scss +2 -0
  46. package/livingdocs/080.CV/010.cv/scss/editor.scss +10 -1
  47. package/livingdocs/080.CV/010.cv/scss/web.scss +1 -1
  48. package/livingdocs/080.CV/020.cv-time-span/scss/_spacing-variations.scss +0 -2
  49. package/livingdocs/100.Misc/010.anchor/scss/editor.scss +4 -0
  50. package/livingdocs/100.Misc/020.accordion/accordion.html +9 -9
  51. package/livingdocs/110.PDF/010.pdf-pagebreak/scss/editor.scss +1 -0
  52. package/livingdocs/110.PDF/021.pdf-columnbreak/scss/editor.scss +1 -0
  53. package/livingdocs/110.PDF/070.pdf-cover/properties.json +2 -2
  54. package/livingdocs/110.PDF/070.pdf-cover/scss/general.scss +3 -0
  55. package/livingdocs/110.PDF/100.pdf-toc-item/scss/general.scss +6 -8
  56. package/livingdocs/120.Startpage/010.hero-video/hero-video.html +10 -0
  57. package/livingdocs/120.Startpage/010.hero-video/ld-conf.json +5 -0
  58. package/livingdocs/120.Startpage/010.hero-video/properties.json +1 -0
  59. package/livingdocs/120.Startpage/010.hero-video/scss/_spacing-variations.scss +3 -0
  60. package/livingdocs/120.Startpage/010.hero-video/scss/app.scss +2 -0
  61. package/livingdocs/120.Startpage/010.hero-video/scss/editor.scss +9 -0
  62. package/livingdocs/120.Startpage/010.hero-video/scss/web.scss +78 -0
  63. package/livingdocs/120.Startpage/020.teaser-title-image-quote/ld-conf.json +18 -0
  64. package/livingdocs/120.Startpage/020.teaser-title-image-quote/properties.json +1 -0
  65. package/livingdocs/120.Startpage/020.teaser-title-image-quote/scss/_spacing-variations.scss +4 -0
  66. package/livingdocs/120.Startpage/020.teaser-title-image-quote/scss/app.scss +2 -0
  67. package/livingdocs/120.Startpage/020.teaser-title-image-quote/scss/editor.scss +22 -0
  68. package/livingdocs/120.Startpage/020.teaser-title-image-quote/scss/web.scss +41 -0
  69. package/livingdocs/120.Startpage/020.teaser-title-image-quote/teaser-title-image-quote.html +45 -0
  70. package/livingdocs/120.Startpage/030.teaser-quote/ld-conf.json +25 -0
  71. package/livingdocs/120.Startpage/030.teaser-quote/properties.json +1 -0
  72. package/livingdocs/120.Startpage/030.teaser-quote/scss/_spacing-variations.scss +3 -0
  73. package/livingdocs/120.Startpage/030.teaser-quote/scss/app.scss +2 -0
  74. package/livingdocs/120.Startpage/030.teaser-quote/scss/editor.scss +6 -0
  75. package/livingdocs/120.Startpage/030.teaser-quote/scss/web.scss +126 -0
  76. package/livingdocs/120.Startpage/030.teaser-quote/teaser-quote.html +48 -0
  77. package/livingdocs/120.Startpage/040.teaser/ld-conf.json +23 -0
  78. package/livingdocs/120.Startpage/040.teaser/scss/_spacing-variations.scss +3 -0
  79. package/livingdocs/120.Startpage/040.teaser/scss/app.scss +2 -0
  80. package/livingdocs/120.Startpage/040.teaser/scss/editor.scss +6 -0
  81. package/livingdocs/120.Startpage/040.teaser/scss/web.scss +73 -0
  82. package/livingdocs/120.Startpage/040.teaser/teaser.html +34 -0
  83. package/livingdocs/130.Hosting_Components/010.download-center/download-center.html +5 -1
  84. package/livingdocs/130.Hosting_Components/020.search/scss/web.scss +7 -44
  85. package/livingdocs/130.Hosting_Components/020.search/search.html +7 -1
  86. package/livingdocs/130.Hosting_Components/020.search/search.vue +1 -1
  87. package/livingdocs/130.Hosting_Components/020.search/searchHighlightOnTarget.vue +246 -0
  88. package/livingdocs/999.Properties/font-color/properties.json +15 -0
  89. package/package.json +1 -1
  90. package/scripts/config.js +2 -0
  91. package/scss/spacer/mixins.scss +64 -12
  92. package/srl/srl/fa/index.scss +2 -2
  93. package/srl/srl/fa/source-free.scss +1 -4
  94. package/srl/srl/pdf/PDFNestedContainers.ts +110 -0
  95. package/srl/srl/pdf/PDFNotes.ts +4 -2
  96. package/srl/srl/pdf/PDFSetPageNumbers.ts +3 -1
@@ -0,0 +1,2 @@
1
+ @use "srl";
2
+ @use "web";
@@ -0,0 +1,6 @@
1
+ @use "srl";
2
+ @use "web";
3
+
4
+ .srl-teaser-quote__alt-text {
5
+ grid-column: content-start / content-end;
6
+ }
@@ -0,0 +1,126 @@
1
+ @use "srl";
2
+ @use "@/assets/scss/margins";
3
+ @use "spacing-variations";
4
+
5
+ @include margins.srl-component-margin(spacing-variations.$margins);
6
+
7
+ .srl-teaser-quote {
8
+ position: relative;
9
+ @extend %srl-breakout-grid-base;
10
+ }
11
+
12
+ .srl-teaser-quote__image-container {
13
+ aspect-ratio: 393 / 702;
14
+
15
+ @include srl.grid-media-up(tablet-pt) {
16
+ position: relative;
17
+ z-index: 0;
18
+ aspect-ratio: 834 / 702;
19
+ overflow: clip;
20
+ grid-row: 1;
21
+ grid-column: full-start / full-end;
22
+ }
23
+
24
+ @include srl.grid-media-up(desktop-large) {
25
+ aspect-ratio: 1728 / 702;
26
+ }
27
+ }
28
+
29
+ .srl-teaser-quote__image {
30
+ width: 100%;
31
+ height: 100%;
32
+ object-fit: cover;
33
+ display: block;
34
+ }
35
+
36
+ .srl-teaser-quote__image-container:has(.srl-teaser-quote__image--mobile) {
37
+ .srl-teaser-quote__image--desktop {
38
+ display: none;
39
+ }
40
+
41
+ .srl-teaser-quote__image--mobile {
42
+ display: block;
43
+ }
44
+
45
+ @include srl.grid-media-up(tablet-pt) {
46
+ .srl-teaser-quote__image--desktop {
47
+ display: block;
48
+ }
49
+ .srl-teaser-quote__image--mobile {
50
+ display: none;
51
+ }
52
+ }
53
+ }
54
+
55
+ .srl-teaser-quote__title-container,
56
+ .srl-teaser-quote__content-container {
57
+ // Mobile
58
+ @include srl.grid-col(4);
59
+
60
+ // Mobile landscape
61
+ @include srl.grid-col(4, phone-ls);
62
+
63
+ // Tablet portrait
64
+ @include srl.grid-col(8, tablet-pt);
65
+ }
66
+
67
+ .srl-teaser-quote__content-container {
68
+ position: absolute;
69
+ top: 0;
70
+ right: 0;
71
+ bottom: 0;
72
+ left: 0;
73
+ width: 100%;
74
+ display: flex;
75
+ flex-direction: column;
76
+ justify-content: space-between;
77
+ margin-top: var(--srl-container-padding, unset);
78
+ margin-bottom: var(--srl-container-padding, unset);
79
+ padding-inline: var(--srl-container-padding);
80
+
81
+ @include srl.grid-media-up(tablet-ls) {
82
+ padding-inline: 0;
83
+ grid-column: content-start / content-end;
84
+ display: grid;
85
+ grid-template-columns: subgrid;
86
+ position: relative;
87
+ top: auto;
88
+ right: auto;
89
+ bottom: auto;
90
+ left: auto;
91
+ grid-row: 1;
92
+ margin-top: srl.system-size-unit(106);
93
+ margin-bottom: srl.system-size-unit(106);
94
+ }
95
+
96
+ @include srl.grid-media-up(desktop) {
97
+ margin-top: srl.spacer-get(800);
98
+ margin-bottom: srl.spacer-get(800);
99
+ }
100
+ }
101
+
102
+ .srl-teaser-quote__content {
103
+ padding: srl.spacer-get(300);
104
+
105
+ @include srl.grid-media-up(tablet-ls) {
106
+ align-self: end;
107
+ grid-column: col 4 / content-end;
108
+ }
109
+
110
+ @include srl.grid-media-up(desktop) {
111
+ align-self: end;
112
+ z-index: 1;
113
+ grid-column: col 7 / content-end;
114
+ border-radius: 16px;
115
+
116
+ .srl-reverse & {
117
+ grid-column: content-start / col 7;
118
+ }
119
+ }
120
+ }
121
+
122
+ .srl-teaser-quote__title-container {
123
+ @include srl.grid-media-up(tablet-ls) {
124
+ grid-column: content-start / content-end;
125
+ }
126
+ }
@@ -0,0 +1,48 @@
1
+ <div class="srl-teaser-quote">
2
+ <div class="srl-teaser-quote__alt-text" doc-editable="alt-tag" doc-toggle="show-alt-text"
3
+ data-alt-text-source="alt">
4
+ Write the alt text here (optional, but recommended)
5
+ </div>
6
+
7
+ <div class="srl-teaser-quote__image-container">
8
+ <img class="srl-teaser-quote__image srl-teaser-quote__image--desktop" doc-image="image" data-alt-text-target="alt">
9
+ <img class="srl-teaser-quote__image srl-teaser-quote__image--mobile" doc-image="image-mobile" data-alt-text-target="alt">
10
+ </div>
11
+
12
+ <div class="srl-teaser-quote__content-container">
13
+ <div class="srl-teaser-quote__title-container">
14
+ <h2 class="srl-title-h2">
15
+ <span class="srl-title-h2__number-text-container">
16
+ <span class="srl-title-h2__text" doc-editable="title-h2">
17
+ Continually coordinate interactive infrastructures for goal-oriented
18
+ </span>
19
+ </span>
20
+ </h2>
21
+ </div>
22
+ <div class="srl-teaser-quote__content srl-bg-sand-1000">
23
+ <blockquote class="srl-quote">
24
+ <div class="srl-quote__text">
25
+ <p class="srl-quote__quote srl-linkable" doc-editable="quote-text">
26
+ Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse
27
+ </p>
28
+ <cite class="srl-quote__cite">
29
+ <p class="srl-quote__name" doc-editable="quote-name" doc-optional>
30
+ Max Mustermann
31
+ </p>
32
+ <p class="srl-quote__position" doc-editable="quote-position" doc-optional>
33
+ CEO
34
+ </p>
35
+ </cite>
36
+ </div>
37
+ </blockquote>
38
+ <div class="srl-button-container">
39
+ <div class="srl-button-container__inner">
40
+ <a doc-link="href" class="srl-button srl-button-icon-arrow-right-after">
41
+ <i class="srl-button__icon srl-icon srl-icon-arrow-right" aria-hidden="true"></i>
42
+ <span class="srl-button__text" doc-editable="button-text">Button</span>
43
+ </a>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </div>
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "teaser",
3
+ "label": "Teaser",
4
+ "properties": [
5
+ "icon",
6
+ "reverse"
7
+ ],
8
+ "directives": {
9
+ "show-alt-text": {
10
+ "type": "toggle",
11
+ "label": "Show alt text",
12
+ "default": false
13
+ },
14
+ "image-mobile": {
15
+ "imageRatios": ["16:9"],
16
+ "allowOriginalRatio": false
17
+ },
18
+ "image": {
19
+ "imageRatios": ["16:9"],
20
+ "allowOriginalRatio": false
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,3 @@
1
+ $margins: (
2
+ ('.srl-teaser', '*'): 3200,
3
+ )
@@ -0,0 +1,2 @@
1
+ @use "srl";
2
+ @use "web";
@@ -0,0 +1,6 @@
1
+ @use "srl";
2
+ @use "web";
3
+
4
+ .srl-teaser__alt-text {
5
+ grid-column: content-start / content-end;
6
+ }
@@ -0,0 +1,73 @@
1
+ @use "srl";
2
+ @use "@/assets/scss/margins";
3
+ @use "spacing-variations";
4
+
5
+ .srl-teaser {
6
+ @extend %srl-breakout-grid-base;
7
+ }
8
+
9
+ @include margins.srl-component-margin(spacing-variations.$margins);
10
+
11
+ .srl-teaser__image-container {
12
+ @include srl.grid-media-up(desktop) {
13
+ position: relative;
14
+ z-index: 0;
15
+ aspect-ratio: 16 / 9;
16
+ overflow: clip;
17
+ grid-row: 1;
18
+ grid-column: full-start / col 11;
19
+
20
+ .srl-reverse & {
21
+ grid-column: col 3 / full-end;
22
+ }
23
+ }
24
+ }
25
+
26
+ .srl-teaser__image-container:has(.srl-teaser__image--mobile) {
27
+ .srl-teaser__image--desktop {
28
+ display: none;
29
+ }
30
+
31
+ .srl-teaser__image--mobile {
32
+ display: block;
33
+ }
34
+
35
+ @include srl.grid-media-up(tablet-pt) {
36
+ .srl-teaser__image--desktop {
37
+ display: block;
38
+ }
39
+ .srl-teaser__image--mobile {
40
+ display: none;
41
+ }
42
+ }
43
+ }
44
+
45
+ .srl-teaser__content-container {
46
+ @include srl.grid-media-up(desktop) {
47
+ margin: 0;
48
+ padding: 0;
49
+ grid-column: content-start / content-end;
50
+ display: grid;
51
+ grid-template-columns: subgrid;
52
+ position: relative;
53
+ grid-row: 1;
54
+ }
55
+ }
56
+
57
+ .srl-teaser__content {
58
+ padding: var(--srl-container-padding, unset);
59
+ width: auto;
60
+ grid-column-end: span 4;
61
+
62
+ @include srl.grid-media-up(desktop) {
63
+ padding: var(--srl-spacer-300, unset);
64
+ border-radius: 16px;
65
+ align-self: center;
66
+ z-index: 1;
67
+ grid-column: col 6 / content-end;
68
+
69
+ .srl-reverse & {
70
+ grid-column: content-start / col 8;
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,34 @@
1
+ <div class="srl-teaser">
2
+ <div class="srl-teaser__alt-text" doc-editable="alt-tag" doc-toggle="show-alt-text"
3
+ data-alt-text-source="alt">
4
+ Write the alt text here (optional, but recommended)
5
+ </div>
6
+
7
+ <div class="srl-teaser__image-container">
8
+ <img class="srl-teaser__image srl-teaser__image--desktop" doc-image="image" data-alt-text-target="alt">
9
+ <img class="srl-teaser__image srl-teaser__image--mobile" doc-image="image-mobile" data-alt-text-target="alt">
10
+ </div>
11
+
12
+ <div class="srl-teaser__content-container">
13
+ <div class="srl-teaser__content srl-bg-sand-1000">
14
+ <h2 class="srl-title-h2">
15
+ <span class="srl-title-h2__number-text-container">
16
+ <span class="srl-title-h2__text" doc-editable="title-h2">Vorsorge</span>
17
+ </span>
18
+ </h2>
19
+ <div class="srl-lead">
20
+ <p class="srl-lead__text" doc-editable="lead">
21
+ Lorem ipsum dolor sit amet, consete ur sadipscing elitr, sed diam nonumy.
22
+ </p>
23
+ </div>
24
+ <div class="srl-button-container">
25
+ <div class="srl-button-container__inner">
26
+ <a doc-link="href" class="srl-button srl-button-icon-arrow-right-after">
27
+ <i class="srl-button__icon srl-icon srl-icon-arrow-right" aria-hidden="true"></i>
28
+ <span class="srl-button__text" doc-editable="button-text">Button</span>
29
+ </a>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ </div>
@@ -1,4 +1,8 @@
1
- <srl-ld-download-center>
1
+ <srl-ld-download-center
2
+ data-remove-from-pdf="complete"
3
+ data-remove-from-word="complete"
4
+ data-remove-from-xhtml="complete"
5
+ >
2
6
  <div class="srl-download-center" data-remove-from-web="complete">
3
7
  <span class="srl-download-center__editor-text" data-remove-from-pdf="complete">
4
8
  Placeholder for download list
@@ -16,12 +16,16 @@ Search form
16
16
  display: flex;
17
17
  justify-content: flex-end;
18
18
  align-items: center;
19
- background-color: srl.colors-grey-600();
19
+ background-color: srl.colors-white-1000();
20
+ border: 1px solid srl.colors-grey-200();
20
21
  border-radius: srl.system-root-style(srl-default-border-radius);
21
22
  overflow: hidden;
22
23
 
23
24
  &:hover {
24
25
  @extend %srl-button__switch-hover;
26
+ //background: srl.colors-white-1000();
27
+
28
+ /*
25
29
  background-color: srl.colors-secondary-1000();
26
30
  .srl-search-form__button {
27
31
  background-color: srl.colors-secondary-1000();
@@ -31,48 +35,13 @@ Search form
31
35
  color: srl.colors-grey-800();
32
36
  }
33
37
  }
38
+ */
34
39
  }
35
40
 
36
41
  &:has(input:focus) {
42
+ background: srl.colors-sand-600();
37
43
  @extend %srl-button__switch-focus;
38
- background-color: srl.colors-secondary-1000();
39
- .srl-search-form__input {
40
- color: srl.colors-primary-1000();
41
- &::placeholder {
42
- color: srl.colors-primary-800();
43
- }
44
- }
45
- .srl-search-form__button {
46
- background-color: srl.colors-secondary-1000();
47
- border-color: srl.colors-primary-1000();
48
-
49
- svg {
50
- color: srl.colors-primary-1000();
51
- }
52
- }
53
44
  }
54
-
55
- /*
56
- &:has(input:focus-visible) {
57
- background-color: srl.colors-primary-200();
58
- border: srl.system-size-unit(1) solid srl.colors-primary-1000();
59
- .srl-search-form__input {
60
- color: srl.colors-grey-600();
61
- &::placeholder {
62
- color: srl.colors-grey-400();
63
- }
64
- }
65
- .srl-search-form__button {
66
- background-color: srl.colors-primary-200();
67
- border-color: srl.colors-grey-600();
68
-
69
- svg {
70
- color: srl.colors-grey-600();
71
- }
72
- }
73
- }
74
-
75
- */
76
45
  }
77
46
 
78
47
  .srl-search-form__label {
@@ -118,12 +87,6 @@ Search form
118
87
  .srl-search-form__button {
119
88
  position: absolute;
120
89
  right: srl.spacer-get(400);
121
- background-color: srl.colors-grey-600();
122
- border-color: srl.colors-grey-800();
123
-
124
- svg {
125
- color: srl.colors-grey-800();
126
- }
127
90
  }
128
91
 
129
92
  /*
@@ -1,4 +1,10 @@
1
- <srl-ld-search use-auto-search use-more-results>
1
+ <srl-ld-search
2
+ use-auto-search
3
+ use-more-results
4
+ data-remove-from-pdf="complete"
5
+ data-remove-from-word="complete"
6
+ data-remove-from-xhtml="complete"
7
+ >
2
8
  <div class="srl-search" data-remove-from-web="complete">
3
9
  <span class="srl-search__editor-text">
4
10
  Placeholder for search form
@@ -391,7 +391,7 @@ function setWindowState(index: number) {
391
391
  />
392
392
  <button
393
393
  tabindex="-2"
394
- class="srl-button srl-button--icon"
394
+ class="srl-search-form__button srl-button srl-button--icon"
395
395
  type="button"
396
396
  :aria-label="$t('search.search')"
397
397
  @click="submitSearch"
@@ -0,0 +1,246 @@
1
+ <script setup lang="ts">
2
+ import { nextTick, onBeforeUnmount, onMounted, watch } from 'vue'
3
+ import { useRoute } from 'vue-router'
4
+
5
+ const props = withDefaults(defineProps<{
6
+ rootSelector?: string
7
+ highlightClass?: string
8
+ queryParam?: string
9
+ }>(), {
10
+ rootSelector: '#srl-page-main',
11
+ highlightClass: 'searchTarget',
12
+ queryParam: 'searchTarget'
13
+ })
14
+
15
+ const route = useRoute()
16
+
17
+ let mutationObserver: MutationObserver | null = null
18
+ let highlightTimeout: ReturnType<typeof setTimeout> | null = null
19
+ let isApplyingHighlight = false
20
+ let hasScrolledToHighlight = false
21
+
22
+ function getRootElement(): HTMLElement | null {
23
+ return document.querySelector(props.rootSelector)
24
+ }
25
+
26
+ function escapeRegExp(value: string): string {
27
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
28
+ }
29
+
30
+ function removeHighlights(root: HTMLElement): void {
31
+ const highlights = root.querySelectorAll(`span.${props.highlightClass}`)
32
+
33
+ highlights.forEach((highlight) => {
34
+ const parent = highlight.parentNode
35
+ if (!parent) return
36
+
37
+ parent.replaceChild(document.createTextNode(highlight.textContent ?? ''), highlight)
38
+ parent.normalize()
39
+ })
40
+ }
41
+
42
+ function highlightTextNodes(root: HTMLElement, term: string): number {
43
+ const safeTermPattern = escapeRegExp(term)
44
+ const regex = new RegExp(`(${safeTermPattern})`, 'gi')
45
+
46
+ const walker = document.createTreeWalker(
47
+ root,
48
+ NodeFilter.SHOW_TEXT,
49
+ {
50
+ acceptNode(node) {
51
+ const parent = node.parentElement
52
+ if (!parent) return NodeFilter.FILTER_REJECT
53
+
54
+ const tag = parent.tagName.toLowerCase()
55
+ if (
56
+ tag === 'script' ||
57
+ tag === 'style' ||
58
+ tag === 'noscript' ||
59
+ tag === 'textarea'
60
+ ) {
61
+ return NodeFilter.FILTER_REJECT
62
+ }
63
+
64
+ if (parent.closest(`.${props.highlightClass}`)) {
65
+ return NodeFilter.FILTER_REJECT
66
+ }
67
+
68
+ const text = node.textContent
69
+ if (!text || !text.trim()) return NodeFilter.FILTER_SKIP
70
+
71
+ regex.lastIndex = 0
72
+ if (!regex.test(text)) return NodeFilter.FILTER_SKIP
73
+
74
+ return NodeFilter.FILTER_ACCEPT
75
+ }
76
+ }
77
+ )
78
+
79
+ const textNodes: Text[] = []
80
+ let node: Node | null = null
81
+
82
+ while ((node = walker.nextNode())) {
83
+ textNodes.push(node as Text)
84
+ }
85
+
86
+ let hitCount = 0
87
+
88
+ for (const textNode of textNodes) {
89
+ const text = textNode.textContent ?? ''
90
+ regex.lastIndex = 0
91
+
92
+ const matches = [...text.matchAll(regex)]
93
+ if (!matches.length) continue
94
+
95
+ const fragment = document.createDocumentFragment()
96
+ let lastIndex = 0
97
+
98
+ for (const match of matches) {
99
+ const matchText = match[0]
100
+ const index = match.index ?? 0
101
+
102
+ if (index > lastIndex) {
103
+ fragment.appendChild(document.createTextNode(text.slice(lastIndex, index)))
104
+ }
105
+
106
+ const span = document.createElement('span')
107
+ span.className = props.highlightClass
108
+ span.textContent = matchText
109
+ fragment.appendChild(span)
110
+
111
+ lastIndex = index + matchText.length
112
+ hitCount++
113
+ }
114
+
115
+ if (lastIndex < text.length) {
116
+ fragment.appendChild(document.createTextNode(text.slice(lastIndex)))
117
+ }
118
+
119
+ textNode.parentNode?.replaceChild(fragment, textNode)
120
+ }
121
+
122
+ return hitCount
123
+ }
124
+
125
+ function scrollToFirstHighlight(root: HTMLElement): void {
126
+ const firstHighlight = root.querySelector<HTMLElement>(`.${props.highlightClass}`)
127
+ if (!firstHighlight) return
128
+
129
+ firstHighlight.scrollIntoView({
130
+ behavior: 'smooth',
131
+ block: 'center',
132
+ inline: 'nearest'
133
+ })
134
+ }
135
+
136
+ function stopObserver(): void {
137
+ mutationObserver?.disconnect()
138
+ }
139
+
140
+ function startObserver(): void {
141
+ const root = getRootElement()
142
+ if (!root) return
143
+
144
+ stopObserver()
145
+
146
+ mutationObserver = new MutationObserver(() => {
147
+ if (isApplyingHighlight) return
148
+ scheduleHighlight()
149
+ })
150
+
151
+ mutationObserver.observe(root, {
152
+ childList: true,
153
+ subtree: true,
154
+ characterData: true
155
+ })
156
+ }
157
+
158
+ function scheduleHighlight(): void {
159
+ if (highlightTimeout) {
160
+ clearTimeout(highlightTimeout)
161
+ }
162
+
163
+ highlightTimeout = setTimeout(() => {
164
+ void applySearchHighlight()
165
+ }, 0)
166
+ }
167
+
168
+ async function applySearchHighlight(): Promise<void> {
169
+ const root = getRootElement()
170
+ if (!root || isApplyingHighlight) return
171
+
172
+ const rawSearchTarget = route.query[props.queryParam]
173
+ const searchTarget =
174
+ typeof rawSearchTarget === 'string'
175
+ ? decodeURIComponent(rawSearchTarget).trim()
176
+ : ''
177
+
178
+ isApplyingHighlight = true
179
+ stopObserver()
180
+
181
+ try {
182
+ removeHighlights(root)
183
+
184
+ if (!searchTarget) {
185
+ hasScrolledToHighlight = false
186
+ return
187
+ }
188
+
189
+ await nextTick()
190
+
191
+ const hitCount = highlightTextNodes(root, searchTarget)
192
+
193
+ if (hitCount > 0 && !hasScrolledToHighlight) {
194
+ await nextTick()
195
+ scrollToFirstHighlight(root)
196
+ hasScrolledToHighlight = true
197
+ }
198
+ } finally {
199
+ isApplyingHighlight = false
200
+ startObserver()
201
+ }
202
+ }
203
+
204
+ onMounted(async () => {
205
+ await nextTick()
206
+ startObserver()
207
+ await applySearchHighlight()
208
+ })
209
+
210
+ watch(
211
+ () => route.fullPath,
212
+ async () => {
213
+ hasScrolledToHighlight = false
214
+ await nextTick()
215
+ scheduleHighlight()
216
+ }
217
+ )
218
+
219
+ watch(
220
+ () => props.rootSelector,
221
+ async () => {
222
+ hasScrolledToHighlight = false
223
+ await nextTick()
224
+ startObserver()
225
+ scheduleHighlight()
226
+ }
227
+ )
228
+
229
+ onBeforeUnmount(() => {
230
+ stopObserver()
231
+
232
+ if (highlightTimeout) {
233
+ clearTimeout(highlightTimeout)
234
+ }
235
+ })
236
+ </script>
237
+
238
+ <template />
239
+
240
+ <style lang="scss">
241
+ @use "srl";
242
+ .searchTarget {
243
+ background-color: srl.colors-primary-1000();
244
+ color: srl.colors-white-1000();
245
+ }
246
+ </style>