@bagelink/vue 1.18.7 → 1.19.3
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/dist/components/Dropdown.vue.d.ts.map +1 -1
- package/dist/components/Icon/Icon.vue.d.ts.map +1 -1
- package/dist/components/dataTable/DataTable.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts +1 -0
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/config.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/index.vue.d.ts +6 -0
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/richTextTypes.d.ts +1 -0
- package/dist/components/form/inputs/RichText/richTextTypes.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/contentMetrics.d.ts +7 -0
- package/dist/components/form/inputs/RichText/utils/contentMetrics.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/image.d.ts +17 -0
- package/dist/components/form/inputs/RichText/utils/image.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/sanitizePaste.d.ts +15 -0
- package/dist/components/form/inputs/RichText/utils/sanitizePaste.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/selection.d.ts +1 -1
- package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/video.d.ts +23 -0
- package/dist/components/form/inputs/RichText/utils/video.d.ts.map +1 -0
- package/dist/components/form/inputs/SelectBtn.vue.d.ts +28 -1
- package/dist/components/form/inputs/SelectBtn.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts +2 -0
- package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/Upload/useFileUpload.d.ts +1 -0
- package/dist/components/form/inputs/Upload/useFileUpload.d.ts.map +1 -1
- package/dist/core.css +2 -2
- package/dist/index.cjs +218 -130
- package/dist/index.css +1 -1
- package/dist/index.mjs +18662 -17635
- package/dist/style.css +2 -2
- package/package.json +1 -1
- package/src/components/Dropdown.vue +30 -0
- package/src/components/FilterQuery.vue +1 -1
- package/src/components/Icon/Icon.vue +4 -0
- package/src/components/dataTable/DataTable.vue +6 -3
- package/src/components/form/inputs/RichText/components/EditorToolbar.vue +238 -8
- package/src/components/form/inputs/RichText/composables/useCommands.ts +23 -5
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +28 -0
- package/src/components/form/inputs/RichText/config.ts +17 -14
- package/src/components/form/inputs/RichText/index.vue +381 -478
- package/src/components/form/inputs/RichText/richTextTypes.ts +1 -0
- package/src/components/form/inputs/RichText/utils/commands.ts +437 -24
- package/src/components/form/inputs/RichText/utils/contentMetrics.ts +24 -0
- package/src/components/form/inputs/RichText/utils/image.ts +136 -0
- package/src/components/form/inputs/RichText/utils/sanitizePaste.ts +110 -0
- package/src/components/form/inputs/RichText/utils/selection.ts +32 -0
- package/src/components/form/inputs/RichText/utils/table.ts +77 -8
- package/src/components/form/inputs/RichText/utils/video.ts +270 -0
- package/src/components/form/inputs/SelectBtn.vue +126 -15
- package/src/components/form/inputs/TelInput.vue +1 -1
- package/src/components/form/inputs/Upload/UploadInput.vue +3 -2
- package/src/components/form/inputs/Upload/useFileUpload.ts +2 -0
- package/src/styles/colors.css +5 -2
- package/src/styles/core.css +1 -0
- package/src/styles/rich-content.css +109 -0
- package/dist/components/form/inputs/RichText/utils/linkPolicy.d.ts +0 -11
- package/dist/components/form/inputs/RichText/utils/linkPolicy.d.ts.map +0 -1
- package/src/components/form/inputs/RichText/utils/linkPolicy.ts +0 -48
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { Option } from '../../../
|
|
3
|
-
import
|
|
2
|
+
import type { IconType, Option } from '../../../types'
|
|
3
|
+
import Btn from '../../Btn.vue'
|
|
4
|
+
import Icon from '../../Icon/Icon.vue'
|
|
5
|
+
import { resolveI18n } from '../../../i18n'
|
|
4
6
|
import { computed } from 'vue'
|
|
5
7
|
import { getOptionIcon, getOptionLabel, getOptionValue } from '../../../utils/options'
|
|
6
8
|
import type { BagelInputShellProps } from './bagelInputShell'
|
|
@@ -15,12 +17,38 @@ const props = withDefaults(
|
|
|
15
17
|
thin?: boolean
|
|
16
18
|
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|
17
19
|
multiselect?: boolean
|
|
20
|
+
disabled?: boolean
|
|
21
|
+
/** Show a check icon on selected options. */
|
|
22
|
+
check?: boolean
|
|
23
|
+
/** Inactive-state icon in the reserved check slot. Defaults to a minus. */
|
|
24
|
+
inactiveCheckIcon?: IconType
|
|
25
|
+
/** Legacy compact preset. Prefer explicit height/fontSize in new usage. */
|
|
26
|
+
compact?: boolean
|
|
27
|
+
/** Exact option height; numbers are pixels. Overrides thin/size height. */
|
|
28
|
+
height?: number | string
|
|
29
|
+
mobileHeight?: number | string
|
|
30
|
+
/** Option font size; numbers are pixels. */
|
|
31
|
+
fontSize?: number | string
|
|
32
|
+
mobileFontSize?: number | string
|
|
33
|
+
/** Check icon size, matching Icon's size API (numbers are rem). */
|
|
34
|
+
checkSize?: number | string
|
|
35
|
+
mobileCheckSize?: number | string
|
|
36
|
+
/** Remove the border from each option button. */
|
|
37
|
+
flat?: boolean
|
|
38
|
+
/** Selected option text tone or CSS color. Defaults to white. */
|
|
39
|
+
activeColor?: string
|
|
40
|
+
/** Selected option background tone or CSS color. Defaults to the input active color. */
|
|
41
|
+
activeBgColor?: string
|
|
42
|
+
/** Unselected option text tone or CSS color. */
|
|
43
|
+
inactiveColor?: string
|
|
44
|
+
/** Unselected option background tone or CSS color. */
|
|
45
|
+
inactiveBgColor?: string
|
|
18
46
|
/** Render only each option's icon (label becomes a tooltip). */
|
|
19
47
|
iconOnly?: boolean
|
|
20
48
|
/** Background applied to the options area only; the field label stays outside it. */
|
|
21
49
|
bgColor?: string
|
|
22
50
|
} & BagelInputShellProps>(),
|
|
23
|
-
{ multiselect: false, iconOnly: false },
|
|
51
|
+
{ multiselect: false, iconOnly: false, inactiveCheckIcon: 'remove' },
|
|
24
52
|
)
|
|
25
53
|
|
|
26
54
|
const { shellClass, shellStyle } = useBagelInputShell(() => ({
|
|
@@ -37,11 +65,29 @@ const hasValue = computed(() => {
|
|
|
37
65
|
})
|
|
38
66
|
|
|
39
67
|
/** Theme token names resolve to Bagel CSS variables; raw CSS values pass through. */
|
|
40
|
-
const resolvedBgColor = computed(() =>
|
|
41
|
-
|
|
68
|
+
const resolvedBgColor = computed(() => resolveColor(props.bgColor))
|
|
69
|
+
|
|
70
|
+
function resolveColor(color?: string) {
|
|
42
71
|
if (!color) { return undefined }
|
|
43
|
-
|
|
44
|
-
|
|
72
|
+
if (['transparent', 'inherit', 'currentColor'].includes(color)) { return color }
|
|
73
|
+
const tone = color.replace(/^--bgl-|^bgl-/, '')
|
|
74
|
+
return /^[a-z][a-z0-9-]*$/i.test(tone) ? `var(--bgl-${tone})` : color
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function cssLength(value?: number | string) {
|
|
78
|
+
return typeof value === 'number' ? `${value}px` : value
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const optionStyle = computed(() => ({
|
|
82
|
+
'--bgl-select-btn-active-color': resolveColor(props.activeColor),
|
|
83
|
+
'--bgl-select-btn-active-bg': resolveColor(props.activeBgColor),
|
|
84
|
+
'--bgl-select-btn-inactive-color': resolveColor(props.inactiveColor),
|
|
85
|
+
'--bgl-select-btn-inactive-bg': resolveColor(props.inactiveBgColor),
|
|
86
|
+
'--bgl-select-btn-height': cssLength(props.height),
|
|
87
|
+
'--bgl-select-btn-mobile-height': cssLength(props.mobileHeight),
|
|
88
|
+
'--bgl-select-btn-font-size': cssLength(props.fontSize),
|
|
89
|
+
'--bgl-select-btn-mobile-font-size': cssLength(props.mobileFontSize),
|
|
90
|
+
}))
|
|
45
91
|
|
|
46
92
|
type OptionValue = string | number | boolean
|
|
47
93
|
|
|
@@ -64,7 +110,17 @@ function isSelected(val: OptionValue) {
|
|
|
64
110
|
|
|
65
111
|
<template>
|
|
66
112
|
<div
|
|
67
|
-
class="bagel-input SelectBtn"
|
|
113
|
+
class="bagel-input SelectBtn"
|
|
114
|
+
:class="[shellClass, {
|
|
115
|
+
'has-value': hasValue,
|
|
116
|
+
'is-compact': compact,
|
|
117
|
+
'is-flat': flat,
|
|
118
|
+
'has-custom-height': height != null,
|
|
119
|
+
'has-custom-font-size': fontSize != null,
|
|
120
|
+
'has-mobile-height': mobileHeight != null,
|
|
121
|
+
'has-mobile-font-size': mobileFontSize != null,
|
|
122
|
+
}]"
|
|
123
|
+
:style="[shellStyle, optionStyle]"
|
|
68
124
|
>
|
|
69
125
|
<label v-if="label || required" >
|
|
70
126
|
{{ resolveI18n(label) }}<span v-if="required"> *</span>
|
|
@@ -77,24 +133,46 @@ function isSelected(val: OptionValue) {
|
|
|
77
133
|
<Btn
|
|
78
134
|
v-for="option in options" :key="String(getOptionValue(option))" :thin="thin"
|
|
79
135
|
:size="size"
|
|
136
|
+
:disabled="disabled"
|
|
80
137
|
type="button"
|
|
81
|
-
:class="
|
|
138
|
+
:class="[
|
|
139
|
+
isSelected(getOptionValue(option)!) ? 'is-selected' : 'is-unselected',
|
|
140
|
+
{ 'bg-transparent': !isSelected(getOptionValue(option)!) && !inactiveBgColor },
|
|
141
|
+
]"
|
|
142
|
+
:aria-pressed="isSelected(getOptionValue(option)!)"
|
|
82
143
|
:style="isSelected(getOptionValue(option)!)
|
|
83
144
|
? {
|
|
84
|
-
backgroundColor: 'var(--bgl-input-label-active-color, var(--bgl-primary))',
|
|
85
|
-
borderColor: 'var(--bgl-input-label-active-color, var(--bgl-primary))',
|
|
86
|
-
color: 'white',
|
|
145
|
+
backgroundColor: 'var(--bgl-select-btn-active-bg, var(--bgl-input-label-active-color, var(--bgl-primary)))',
|
|
146
|
+
borderColor: 'var(--bgl-select-btn-active-bg, var(--bgl-input-label-active-color, var(--bgl-primary)))',
|
|
147
|
+
color: 'var(--bgl-select-btn-active-color, white)',
|
|
87
148
|
}
|
|
88
149
|
: {
|
|
89
|
-
color: 'var(--bgl-input-label-active-color, var(--bgl-border-color))',
|
|
150
|
+
color: 'var(--bgl-select-btn-inactive-color, var(--bgl-input-label-active-color, var(--bgl-border-color)))',
|
|
90
151
|
borderColor: 'var(--bgl-input-label-active-color, var(--bgl-border-color))',
|
|
152
|
+
backgroundColor: 'var(--bgl-select-btn-inactive-bg, transparent)',
|
|
91
153
|
}"
|
|
92
154
|
class="border radius-2 SelectBtn-padding"
|
|
93
155
|
v-tooltip="iconOnly && getOptionIcon(option) ? getOptionLabel(option) : undefined"
|
|
94
156
|
@click="toggleOption(getOptionValue(option)!)"
|
|
95
157
|
>
|
|
96
|
-
<
|
|
97
|
-
|
|
158
|
+
<span :class="check ? 'flex gap-025' : 'contents'">
|
|
159
|
+
<span
|
|
160
|
+
v-if="check"
|
|
161
|
+
class="flex-center"
|
|
162
|
+
aria-hidden="true"
|
|
163
|
+
:style="{ opacity: isSelected(getOptionValue(option)!) ? 1 : inactiveCheckIcon ? 0.5 : 0 }"
|
|
164
|
+
>
|
|
165
|
+
<Icon
|
|
166
|
+
:icon="isSelected(getOptionValue(option)!) ? 'check' : inactiveCheckIcon ?? 'check'"
|
|
167
|
+
:transition="false"
|
|
168
|
+
weight="500"
|
|
169
|
+
:size="checkSize ?? 0.8"
|
|
170
|
+
:mobile-size="mobileCheckSize"
|
|
171
|
+
/>
|
|
172
|
+
</span>
|
|
173
|
+
<Icon v-if="getOptionIcon(option)" :icon="getOptionIcon(option)!" weight="300" />
|
|
174
|
+
<span v-if="!(iconOnly && getOptionIcon(option))">{{ getOptionLabel(option) }}</span>
|
|
175
|
+
</span>
|
|
98
176
|
</Btn>
|
|
99
177
|
</div>
|
|
100
178
|
<p v-if="error" class="txt-12 color-error mt-025">
|
|
@@ -114,6 +192,20 @@ function isSelected(val: OptionValue) {
|
|
|
114
192
|
padding: 0 var(--bgl-select-btn-padding, 0.7rem);
|
|
115
193
|
}
|
|
116
194
|
|
|
195
|
+
.SelectBtn.is-compact {
|
|
196
|
+
--bgl-input-font-size: 12px;
|
|
197
|
+
--bgl-input-height: 30px;
|
|
198
|
+
--bgl-btn-height: 30px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.SelectBtn.is-compact .SelectBtn-padding {
|
|
202
|
+
line-height: 1;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.SelectBtn.is-flat .SelectBtn-padding {
|
|
206
|
+
border: none !important;
|
|
207
|
+
}
|
|
208
|
+
|
|
117
209
|
.select-btn-items {
|
|
118
210
|
width: fit-content;
|
|
119
211
|
max-width: 100%;
|
|
@@ -149,4 +241,23 @@ function isSelected(val: OptionValue) {
|
|
|
149
241
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--bgl-input-label-active-color, var(--bgl-primary)) 12%, transparent);
|
|
150
242
|
}
|
|
151
243
|
|
|
244
|
+
.SelectBtn.has-custom-height .SelectBtn-padding {
|
|
245
|
+
height: var(--bgl-select-btn-height);
|
|
246
|
+
line-height: var(--bgl-select-btn-height);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.SelectBtn.has-custom-font-size .SelectBtn-padding {
|
|
250
|
+
font-size: var(--bgl-select-btn-font-size);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@media screen and (max-width: 910px) {
|
|
254
|
+
.SelectBtn.has-mobile-height .SelectBtn-padding {
|
|
255
|
+
height: var(--bgl-select-btn-mobile-height);
|
|
256
|
+
line-height: var(--bgl-select-btn-mobile-height);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.SelectBtn.has-mobile-font-size .SelectBtn-padding {
|
|
260
|
+
font-size: var(--bgl-select-btn-mobile-font-size);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
152
263
|
</style>
|
|
@@ -19,7 +19,7 @@ const props = withDefaults(defineProps<UploadInputProps>(), {
|
|
|
19
19
|
disabled: false,
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
-
const emit = defineEmits(['update:modelValue', 'addFileStart'])
|
|
22
|
+
const emit = defineEmits(['update:modelValue', 'addFileStart', 'uploadError'])
|
|
23
23
|
|
|
24
24
|
const { $t } = useI18n()
|
|
25
25
|
const slots = useSlots()
|
|
@@ -43,7 +43,8 @@ const {
|
|
|
43
43
|
await flushQueue()
|
|
44
44
|
updateModelValue()
|
|
45
45
|
},
|
|
46
|
-
onChange: updateModelValue
|
|
46
|
+
onChange: updateModelValue,
|
|
47
|
+
onUploadError: (error, file) => emit('uploadError', error, file),
|
|
47
48
|
})
|
|
48
49
|
|
|
49
50
|
// Normalize each entry to a path-key/URL string. Legacy/migrated data may store
|
|
@@ -13,6 +13,7 @@ interface UseFileUploadProps {
|
|
|
13
13
|
upload?: (file: File) => Promise<string>
|
|
14
14
|
onFileQueued?: (files: QueueFile[]) => void
|
|
15
15
|
onChange?: () => void
|
|
16
|
+
onUploadError?: (error: unknown, file: File) => void
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function useFileUpload(props: UseFileUploadProps = {}) {
|
|
@@ -126,6 +127,7 @@ export function useFileUpload(props: UseFileUploadProps = {}) {
|
|
|
126
127
|
uploadedPathKeys.push(data.path_key)
|
|
127
128
|
} catch (error) {
|
|
128
129
|
console.error('Error uploading file:', error)
|
|
130
|
+
props.onUploadError?.(error, file.file)
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
133
|
|
package/src/styles/colors.css
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
.bg-input-white input,
|
|
12
12
|
.bg-input-white textarea,
|
|
13
|
-
.bg-input-white .selectinput-btn
|
|
13
|
+
.bg-input-white .selectinput-btn,
|
|
14
|
+
.bg-input-white .tel-input {
|
|
14
15
|
background: var(--bgl-white) !important;
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -666,7 +667,9 @@
|
|
|
666
667
|
border-color: var(--bgl-gray) !important;
|
|
667
668
|
}
|
|
668
669
|
|
|
669
|
-
.color-input-gray input
|
|
670
|
+
.color-input-gray input,
|
|
671
|
+
.color-input-gray .tel-input,
|
|
672
|
+
.color-input-gray .dial-code {
|
|
670
673
|
color: var(--bgl-gray) !important;
|
|
671
674
|
}
|
|
672
675
|
|
package/src/styles/core.css
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
@import "appearance.css";
|
|
26
26
|
@import "gradients.css";
|
|
27
27
|
@import "color-variants.css";
|
|
28
|
+
@import "rich-content.css";
|
|
28
29
|
|
|
29
30
|
.bgl_icon-font { font-family: 'Material Symbols Outlined', serif !important; }
|
|
30
31
|
.bgl_icon-font.fa-brands { font-family: 'Font Awesome 6 Brands', serif !important; }
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/* Public rendering contract for HTML produced by RichText.
|
|
2
|
+
`.bgl-rich-content` is the preferred wrapper. The attribute selectors also
|
|
3
|
+
keep media responsive when saved HTML is rendered without a wrapper. */
|
|
4
|
+
|
|
5
|
+
:where(.bgl-rich-content, .rich-content, .richtxt) {
|
|
6
|
+
display: flow-root;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
:where(.bgl-rich-content, .rich-content, .richtxt) table {
|
|
10
|
+
display: block;
|
|
11
|
+
max-width: 100%;
|
|
12
|
+
overflow-x: auto;
|
|
13
|
+
border-collapse: collapse;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
figure[data-media-type='image'],
|
|
17
|
+
figure[data-media-type='video'],
|
|
18
|
+
figure:has(> iframe[data-media-type='embed']) {
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
max-width: 100%;
|
|
21
|
+
margin-block: 1.5rem;
|
|
22
|
+
margin-inline: 0;
|
|
23
|
+
text-align: center;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
figure[data-media-type='image'] > a {
|
|
27
|
+
display: block;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
figure[data-media-type='image'] img,
|
|
31
|
+
figure[data-media-type='video'] video {
|
|
32
|
+
display: block;
|
|
33
|
+
width: 100%;
|
|
34
|
+
max-width: 100%;
|
|
35
|
+
height: auto;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
figure.align-full {
|
|
39
|
+
float: none;
|
|
40
|
+
width: 100% !important;
|
|
41
|
+
max-width: 100%;
|
|
42
|
+
margin-inline: 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
figure.align-start,
|
|
46
|
+
figure.align-end {
|
|
47
|
+
width: 50% !important;
|
|
48
|
+
max-width: 50%;
|
|
49
|
+
margin-block: 0.25em 0.75em;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
figure.align-start {
|
|
53
|
+
float: inline-start;
|
|
54
|
+
margin-inline: 0 1.25em;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
figure.align-end {
|
|
58
|
+
float: inline-end;
|
|
59
|
+
margin-inline: 1.25em 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.video-responsive {
|
|
63
|
+
position: relative;
|
|
64
|
+
width: 100%;
|
|
65
|
+
height: 0;
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.video-responsive > iframe {
|
|
70
|
+
position: absolute;
|
|
71
|
+
inset: 0;
|
|
72
|
+
width: 100%;
|
|
73
|
+
height: 100%;
|
|
74
|
+
border: 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
figure:has(> iframe[data-media-type='embed']) > iframe {
|
|
78
|
+
display: block;
|
|
79
|
+
width: 100%;
|
|
80
|
+
max-width: 100%;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
figure[data-media-type] figcaption,
|
|
84
|
+
figure:has(> iframe[data-media-type='embed']) figcaption {
|
|
85
|
+
padding-block-start: 0.5rem;
|
|
86
|
+
color: var(--bgl-gray);
|
|
87
|
+
font-size: 0.875rem;
|
|
88
|
+
line-height: 1.4;
|
|
89
|
+
text-align: center;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
figure[data-media-type] figcaption .alt-text {
|
|
93
|
+
color: var(--bgl-text-color);
|
|
94
|
+
font-weight: 600;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
figure[data-media-type] figcaption .photo-credit {
|
|
98
|
+
color: inherit;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@media screen and (max-width: 600px) {
|
|
102
|
+
figure.align-start,
|
|
103
|
+
figure.align-end {
|
|
104
|
+
float: none;
|
|
105
|
+
width: 100% !important;
|
|
106
|
+
max-width: 100%;
|
|
107
|
+
margin-inline: 0;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validate and normalize a link authored in RichText.
|
|
3
|
-
*
|
|
4
|
-
* Relative application paths must be root-relative (`/contact`). Ambiguous
|
|
5
|
-
* imported CMS links (`edit?...`, `admin/...`) are rejected instead of being
|
|
6
|
-
* silently resolved against the current page.
|
|
7
|
-
*/
|
|
8
|
-
export declare function normalizeRichTextLink(value: unknown): string | null;
|
|
9
|
-
/** Remove invalid destinations while preserving their visible linked content. */
|
|
10
|
-
export declare function sanitizeRichTextLinks(root: ParentNode): number;
|
|
11
|
-
//# sourceMappingURL=linkPolicy.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"linkPolicy.d.ts","sourceRoot":"","sources":["../../../../../../src/components/form/inputs/RichText/utils/linkPolicy.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAoBnE;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAc9D"}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
const EXPLICIT_SAFE_URL = /^(?:https?:\/\/|mailto:|tel:)/i
|
|
2
|
-
const BARE_DOMAIN = /^(?:localhost(?::\d+)?|(?:[a-z0-9-]+\.)+[a-z]{2,})(?:[/:?#][^\s]*)?$/i
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Validate and normalize a link authored in RichText.
|
|
6
|
-
*
|
|
7
|
-
* Relative application paths must be root-relative (`/contact`). Ambiguous
|
|
8
|
-
* imported CMS links (`edit?...`, `admin/...`) are rejected instead of being
|
|
9
|
-
* silently resolved against the current page.
|
|
10
|
-
*/
|
|
11
|
-
export function normalizeRichTextLink(value: unknown): string | null {
|
|
12
|
-
if (typeof value !== 'string') return null
|
|
13
|
-
const url = value.trim()
|
|
14
|
-
if (!url || /[\u0000-\u001F\u007F]/.test(url)) return null
|
|
15
|
-
|
|
16
|
-
if (url.startsWith('/') && !url.startsWith('//')) return url
|
|
17
|
-
if (url.startsWith('#') && url.length > 1) return url
|
|
18
|
-
|
|
19
|
-
if (EXPLICIT_SAFE_URL.test(url)) {
|
|
20
|
-
if (/^mailto:/i.test(url)) return url.slice(7).trim() ? url : null
|
|
21
|
-
if (/^tel:/i.test(url)) return url.slice(4).trim() ? url : null
|
|
22
|
-
try {
|
|
23
|
-
const parsed = new URL(url)
|
|
24
|
-
return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? url : null
|
|
25
|
-
} catch {
|
|
26
|
-
return null
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return BARE_DOMAIN.test(url) ? `https://${url}` : null
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Remove invalid destinations while preserving their visible linked content. */
|
|
34
|
-
export function sanitizeRichTextLinks(root: ParentNode): number {
|
|
35
|
-
let removed = 0
|
|
36
|
-
for (const anchor of Array.from(root.querySelectorAll<HTMLAnchorElement>('a[href]'))) {
|
|
37
|
-
const normalized = normalizeRichTextLink(anchor.getAttribute('href'))
|
|
38
|
-
if (normalized) {
|
|
39
|
-
anchor.setAttribute('href', normalized)
|
|
40
|
-
continue
|
|
41
|
-
}
|
|
42
|
-
anchor.removeAttribute('href')
|
|
43
|
-
anchor.removeAttribute('target')
|
|
44
|
-
anchor.removeAttribute('rel')
|
|
45
|
-
removed++
|
|
46
|
-
}
|
|
47
|
-
return removed
|
|
48
|
-
}
|