@gitlab/ui 110.1.0 → 111.0.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/CHANGELOG.md +15 -0
- package/dist/components/base/link/link.js +254 -10
- package/dist/directives/safe_link/safe_link.js +6 -4
- package/dist/utils/constants.js +3 -1
- package/dist/utils/is_slot_empty.js +1 -1
- package/package.json +6 -14
- package/src/components/base/link/link.md +109 -0
- package/src/components/base/link/link.vue +283 -18
- package/src/directives/safe_link/safe_link.js +7 -3
- package/src/utils/constants.js +3 -0
- package/src/utils/is_slot_empty.js +1 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# [111.0.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v110.1.0...v111.0.0) (2025-03-12)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **GlLink:** Remove `BLink` from `GlLink` ([22f8323](https://gitlab.com/gitlab-org/gitlab-ui/commit/22f8323023ed9d44e2af3705c12f02ba93edd506))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### BREAKING CHANGES
|
|
10
|
+
|
|
11
|
+
* **GlLink:** Removed support for the following props:
|
|
12
|
+
`append`, `event`, `exact`, `exact-path`, `exact-path-active-class`,
|
|
13
|
+
`no-prefetch`, `router-component-name`, `router-tag`.
|
|
14
|
+
Also removed support for `bv::link::clicked` global event.
|
|
15
|
+
|
|
1
16
|
# [110.1.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v110.0.0...v110.1.0) (2025-03-10)
|
|
2
17
|
|
|
3
18
|
|
|
@@ -1,17 +1,127 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
1
|
+
import isFunction from 'lodash/isFunction';
|
|
2
|
+
import isString from 'lodash/isString';
|
|
3
|
+
import isObject from 'lodash/isObject';
|
|
4
|
+
import toString from 'lodash/toString';
|
|
5
|
+
import isBoolean from 'lodash/isBoolean';
|
|
6
|
+
import concat from 'lodash/concat';
|
|
7
|
+
import { SafeLinkDirective, isExternalURL } from '../../../directives/safe_link/safe_link';
|
|
8
|
+
import { stopEvent } from '../../../utils/utils';
|
|
9
|
+
import { isEvent } from '../../../vendor/bootstrap-vue/src/utils/inspect';
|
|
10
|
+
import { stringifyQueryObj } from '../../../vendor/bootstrap-vue/src/utils/router';
|
|
11
|
+
import { safeVueInstance } from '../../../vendor/bootstrap-vue/src/utils/safe-vue-instance';
|
|
12
|
+
import { attemptFocus, attemptBlur } from '../../../vendor/bootstrap-vue/src/utils/dom';
|
|
13
|
+
import { linkVariantOptions, isVue3 } from '../../../utils/constants';
|
|
5
14
|
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
|
|
6
15
|
|
|
7
16
|
//
|
|
17
|
+
const ANCHOR_TAG = 'a';
|
|
18
|
+
const NUXT_LINK_TAG = 'nuxt-link';
|
|
19
|
+
const VUE_ROUTER_LINK_TAG = 'router-link';
|
|
8
20
|
var script = {
|
|
9
21
|
name: 'GlLink',
|
|
10
|
-
|
|
11
|
-
|
|
22
|
+
directives: {
|
|
23
|
+
SafeLink: SafeLinkDirective
|
|
12
24
|
},
|
|
13
|
-
mixins: [SafeLinkMixin],
|
|
14
25
|
props: {
|
|
26
|
+
/**
|
|
27
|
+
* Denotes the target URL of the link for standard links.
|
|
28
|
+
*/
|
|
29
|
+
href: {
|
|
30
|
+
type: String,
|
|
31
|
+
required: false,
|
|
32
|
+
default: undefined
|
|
33
|
+
},
|
|
34
|
+
/**
|
|
35
|
+
* When set to `true`, disables the component's functionality and places it in a disabled state.
|
|
36
|
+
*/
|
|
37
|
+
disabled: {
|
|
38
|
+
type: Boolean,
|
|
39
|
+
required: false,
|
|
40
|
+
default: false
|
|
41
|
+
},
|
|
42
|
+
/**
|
|
43
|
+
* Skips sanitization of href if true. This should be used sparingly.
|
|
44
|
+
* Consult security team before setting to true.
|
|
45
|
+
*/
|
|
46
|
+
isUnsafeLink: {
|
|
47
|
+
type: Boolean,
|
|
48
|
+
required: false,
|
|
49
|
+
default: false
|
|
50
|
+
},
|
|
51
|
+
/**
|
|
52
|
+
* Sets the 'rel' attribute on the rendered link.
|
|
53
|
+
*/
|
|
54
|
+
rel: {
|
|
55
|
+
type: String,
|
|
56
|
+
required: false,
|
|
57
|
+
default: null
|
|
58
|
+
},
|
|
59
|
+
/**
|
|
60
|
+
* Sets the 'target' attribute on the rendered link.
|
|
61
|
+
*/
|
|
62
|
+
target: {
|
|
63
|
+
type: String,
|
|
64
|
+
required: false,
|
|
65
|
+
default: null
|
|
66
|
+
},
|
|
67
|
+
/**
|
|
68
|
+
* When set to `true`, places the component in the active state with active styling
|
|
69
|
+
*/
|
|
70
|
+
active: {
|
|
71
|
+
type: Boolean,
|
|
72
|
+
required: false,
|
|
73
|
+
default: false
|
|
74
|
+
},
|
|
75
|
+
/**
|
|
76
|
+
* <router-link> prop: Denotes the target route of the link.
|
|
77
|
+
* When clicked, the value of the to prop will be passed to `router.push()` internally,
|
|
78
|
+
* so the value can be either a string or a Location descriptor object.
|
|
79
|
+
*/
|
|
80
|
+
to: {
|
|
81
|
+
type: [Object, String],
|
|
82
|
+
required: false,
|
|
83
|
+
default: undefined
|
|
84
|
+
},
|
|
85
|
+
/**
|
|
86
|
+
* <router-link> prop: Configure the active CSS class applied when the link is active.
|
|
87
|
+
*/
|
|
88
|
+
activeClass: {
|
|
89
|
+
type: String,
|
|
90
|
+
required: false,
|
|
91
|
+
default: undefined
|
|
92
|
+
},
|
|
93
|
+
/**
|
|
94
|
+
* <router-link> prop: Configure the active CSS class applied when the link is active with exact match.
|
|
95
|
+
*/
|
|
96
|
+
exactActiveClass: {
|
|
97
|
+
type: String,
|
|
98
|
+
required: false,
|
|
99
|
+
default: undefined
|
|
100
|
+
},
|
|
101
|
+
/**
|
|
102
|
+
* <router-link> prop: Setting the replace prop will call `router.replace()` instead of `router.push()`
|
|
103
|
+
* when clicked, so the navigation will not leave a history record.
|
|
104
|
+
*/
|
|
105
|
+
replace: {
|
|
106
|
+
type: Boolean,
|
|
107
|
+
required: false,
|
|
108
|
+
default: false
|
|
109
|
+
},
|
|
110
|
+
/**
|
|
111
|
+
* <nuxt-link> prop: To improve the responsiveness of your Nuxt.js applications, when the link will be displayed within the viewport,
|
|
112
|
+
* Nuxt.js will automatically prefetch the code splitted page. Setting `prefetch` to `true` or `false` will overwrite the default value of `router.prefetchLinks`
|
|
113
|
+
*/
|
|
114
|
+
prefetch: {
|
|
115
|
+
type: Boolean,
|
|
116
|
+
required: false,
|
|
117
|
+
// Must be `null` to fall back to the value defined in the
|
|
118
|
+
// `nuxt.config.js` configuration file for `router.prefetchLinks`
|
|
119
|
+
// We convert `null` to `undefined`, so that Nuxt.js will use the
|
|
120
|
+
// compiled default
|
|
121
|
+
// Vue treats `undefined` as default of `false` for Boolean props,
|
|
122
|
+
// so we must set it as `null` here to be a true tri-state prop
|
|
123
|
+
default: null
|
|
124
|
+
},
|
|
15
125
|
/**
|
|
16
126
|
* If inline variant, controls ↗ character visibility
|
|
17
127
|
*/
|
|
@@ -31,14 +141,142 @@ var script = {
|
|
|
31
141
|
}
|
|
32
142
|
},
|
|
33
143
|
computed: {
|
|
144
|
+
safeLinkConfig() {
|
|
145
|
+
return {
|
|
146
|
+
skipSanitization: this.isUnsafeLink
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
tag() {
|
|
150
|
+
const hasRouter = Boolean(safeVueInstance(this).$router);
|
|
151
|
+
const hasNuxt = Boolean(safeVueInstance(this).$nuxt);
|
|
152
|
+
if (!hasRouter || this.disabled || !this.to) {
|
|
153
|
+
return ANCHOR_TAG;
|
|
154
|
+
}
|
|
155
|
+
return hasNuxt ? NUXT_LINK_TAG : VUE_ROUTER_LINK_TAG;
|
|
156
|
+
},
|
|
157
|
+
isRouterLink() {
|
|
158
|
+
return this.tag !== ANCHOR_TAG;
|
|
159
|
+
},
|
|
160
|
+
isVue3RouterLink() {
|
|
161
|
+
return this.tag === VUE_ROUTER_LINK_TAG && isVue3;
|
|
162
|
+
},
|
|
34
163
|
isInlineAndHasExternalIcon() {
|
|
35
|
-
return this.showExternalIcon && this.variant === 'inline' && this
|
|
164
|
+
return this.showExternalIcon && this.variant === 'inline' && this.href && isExternalURL(this.target, this.href);
|
|
165
|
+
},
|
|
166
|
+
computedHref() {
|
|
167
|
+
const fallback = '#';
|
|
168
|
+
const toFallback = '/';
|
|
169
|
+
const {
|
|
170
|
+
to
|
|
171
|
+
} = this;
|
|
172
|
+
|
|
173
|
+
// Return `href` when explicitly provided
|
|
174
|
+
if (this.href) {
|
|
175
|
+
return this.href;
|
|
176
|
+
}
|
|
177
|
+
if (isString(to)) {
|
|
178
|
+
return to || toFallback;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Fallback to `to.path' + `to.query` + `to.hash` prop (if `to` is an object)
|
|
182
|
+
if (isObject(to) && (to.path || to.query || to.hash)) {
|
|
183
|
+
const path = toString(to.path);
|
|
184
|
+
const query = stringifyQueryObj(to.query);
|
|
185
|
+
let hash = toString(to.hash);
|
|
186
|
+
hash = !hash || hash.charAt(0) === '#' ? hash : `#${hash}`;
|
|
187
|
+
return `${path}${query}${hash}` || fallback;
|
|
188
|
+
}
|
|
189
|
+
return fallback;
|
|
190
|
+
},
|
|
191
|
+
computedProps() {
|
|
192
|
+
if (this.isRouterLink) {
|
|
193
|
+
return {
|
|
194
|
+
to: this.to,
|
|
195
|
+
activeClass: this.activeClass,
|
|
196
|
+
exactActiveClass: this.exactActiveClass,
|
|
197
|
+
replace: this.replace,
|
|
198
|
+
...(isBoolean(this.prefetch) ? {
|
|
199
|
+
prefetch: this.prefetch
|
|
200
|
+
} : {})
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
disabled: this.disabled,
|
|
205
|
+
...(this.disabled ? {
|
|
206
|
+
'aria-disabled': 'true',
|
|
207
|
+
tabindex: '-1'
|
|
208
|
+
} : {}),
|
|
209
|
+
rel: this.rel,
|
|
210
|
+
target: this.target,
|
|
211
|
+
href: this.computedHref
|
|
212
|
+
};
|
|
36
213
|
},
|
|
37
|
-
|
|
214
|
+
computedListeners() {
|
|
215
|
+
const {
|
|
216
|
+
click,
|
|
217
|
+
...listenersWithoutClick
|
|
218
|
+
} = this.$listeners;
|
|
219
|
+
return listenersWithoutClick;
|
|
220
|
+
},
|
|
221
|
+
computedClass() {
|
|
38
222
|
return ['gl-link', linkVariantOptions[this.variant], {
|
|
223
|
+
disabled: this.disabled,
|
|
224
|
+
active: this.active,
|
|
39
225
|
'gl-link-inline-external': this.isInlineAndHasExternalIcon
|
|
40
226
|
}];
|
|
41
227
|
}
|
|
228
|
+
},
|
|
229
|
+
methods: {
|
|
230
|
+
onClick(event, navigate) {
|
|
231
|
+
const eventIsEvent = isEvent(event);
|
|
232
|
+
const suppliedHandler = this.$listeners.click;
|
|
233
|
+
if (eventIsEvent && this.disabled) {
|
|
234
|
+
// Stop event from bubbling up
|
|
235
|
+
// Kill the event loop attached to this specific `EventTarget`
|
|
236
|
+
// Needed to prevent `vue-router` from navigating
|
|
237
|
+
stopEvent(event, {
|
|
238
|
+
immediatePropagation: true
|
|
239
|
+
});
|
|
240
|
+
} else {
|
|
241
|
+
// Router links do not emit instance `click` events, so we
|
|
242
|
+
// add in an `$emit('click', event)` on its Vue instance
|
|
243
|
+
//
|
|
244
|
+
// seems not to be required for Vue3 compat build
|
|
245
|
+
if (this.isRouterLink) {
|
|
246
|
+
var _event$currentTarget$;
|
|
247
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
248
|
+
(_event$currentTarget$ = event.currentTarget.__vue__) === null || _event$currentTarget$ === void 0 ? void 0 : _event$currentTarget$.$emit('click', event);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Call the suppliedHandler(s), if any provided
|
|
252
|
+
concat([], suppliedHandler).filter(h => isFunction(h)).forEach(handler => {
|
|
253
|
+
// eslint-disable-next-line prefer-rest-params
|
|
254
|
+
handler(...arguments);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// this navigate function comes from Vue 3 router
|
|
258
|
+
// See https://router.vuejs.org/guide/advanced/extending-router-link.html#Extending-RouterLink
|
|
259
|
+
if (isFunction(navigate)) {
|
|
260
|
+
navigate(event);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// TODO: Remove deprecated 'clicked::link' event
|
|
264
|
+
this.$root.$emit('clicked::link', event);
|
|
265
|
+
}
|
|
266
|
+
// Stop scroll-to-top behavior or navigation on
|
|
267
|
+
// regular links when href is just '#'
|
|
268
|
+
if (eventIsEvent && !this.isRouterLink && this.computedHref === '#') {
|
|
269
|
+
stopEvent(event, {
|
|
270
|
+
stopPropagation: false
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
focus() {
|
|
275
|
+
attemptFocus(this.$el);
|
|
276
|
+
},
|
|
277
|
+
blur() {
|
|
278
|
+
attemptBlur(this.$el);
|
|
279
|
+
}
|
|
42
280
|
}
|
|
43
281
|
};
|
|
44
282
|
|
|
@@ -46,7 +284,13 @@ var script = {
|
|
|
46
284
|
const __vue_script__ = script;
|
|
47
285
|
|
|
48
286
|
/* template */
|
|
49
|
-
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return
|
|
287
|
+
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.isVue3RouterLink)?_c(_vm.tag,_vm._b({tag:"component",attrs:{"custom":""},scopedSlots:_vm._u([{key:"default",fn:function(ref){
|
|
288
|
+
var _obj;
|
|
289
|
+
|
|
290
|
+
var routerLinkHref = ref.href;
|
|
291
|
+
var isActive = ref.isActive;
|
|
292
|
+
var isExactActive = ref.isExactActive;
|
|
293
|
+
var navigate = ref.navigate;return [_c('a',_vm._g({directives:[{name:"safe-link",rawName:"v-safe-link:[safeLinkConfig]",arg:_vm.safeLinkConfig}],class:[_vm.computedClass, ( _obj = {}, _obj[_vm.activeClass] = isActive, _obj[_vm.exactActiveClass] = isExactActive, _obj )],attrs:{"href":routerLinkHref},on:{"click":function($event){return _vm.onClick($event, navigate)}}},_vm.computedListeners),[_vm._t("default")],2)]}}],null,true)},'component',_vm.computedProps,false)):(_vm.isRouterLink)?_c(_vm.tag,_vm._g(_vm._b({directives:[{name:"safe-link",rawName:"v-safe-link:[safeLinkConfig]",arg:_vm.safeLinkConfig}],tag:"component",class:_vm.computedClass,nativeOn:{"click":function($event){return _vm.onClick.apply(null, arguments)}}},'component',_vm.computedProps,false),_vm.computedListeners),[_vm._t("default")],2):_c(_vm.tag,_vm._g(_vm._b({directives:[{name:"safe-link",rawName:"v-safe-link:[safeLinkConfig]",arg:_vm.safeLinkConfig}],tag:"component",class:_vm.computedClass,on:{"click":_vm.onClick}},'component',_vm.computedProps,false),_vm.computedListeners),[_vm._t("default")],2)};
|
|
50
294
|
var __vue_staticRenderFns__ = [];
|
|
51
295
|
|
|
52
296
|
/* style */
|
|
@@ -7,8 +7,11 @@ const getBaseURL = () => {
|
|
|
7
7
|
} = window.location;
|
|
8
8
|
return `${protocol}//${host}`;
|
|
9
9
|
};
|
|
10
|
+
const isTargetBlank = target => {
|
|
11
|
+
return target === '_blank';
|
|
12
|
+
};
|
|
10
13
|
const isExternalURL = (target, hostname) => {
|
|
11
|
-
return target
|
|
14
|
+
return isTargetBlank(target) && hostname !== window.location.hostname;
|
|
12
15
|
};
|
|
13
16
|
const secureRel = rel => {
|
|
14
17
|
const rels = rel ? rel.trim().split(' ') : [];
|
|
@@ -40,13 +43,12 @@ const transform = function (el) {
|
|
|
40
43
|
const {
|
|
41
44
|
href,
|
|
42
45
|
target,
|
|
43
|
-
rel
|
|
44
|
-
hostname
|
|
46
|
+
rel
|
|
45
47
|
} = el;
|
|
46
48
|
if (!isSafeURL(href)) {
|
|
47
49
|
el.href = 'about:blank';
|
|
48
50
|
}
|
|
49
|
-
if (
|
|
51
|
+
if (isTargetBlank(target)) {
|
|
50
52
|
el.rel = secureRel(rel);
|
|
51
53
|
}
|
|
52
54
|
};
|
package/dist/utils/constants.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Vue from 'vue';
|
|
1
2
|
import { POSITION } from '../components/utilities/truncate/constants';
|
|
2
3
|
|
|
3
4
|
const COMMA = ',';
|
|
@@ -316,5 +317,6 @@ const loadingIconVariants = {
|
|
|
316
317
|
spinner: 'spinner',
|
|
317
318
|
dots: 'dots'
|
|
318
319
|
};
|
|
320
|
+
const isVue3 = Boolean(Vue.Fragment);
|
|
319
321
|
|
|
320
|
-
export { COMMA, CONTRAST_LEVELS, HEX_REGEX, LEFT_MOUSE_BUTTON, alertVariantIconMap, alertVariantOptions, alignOptions, animatedIconVariantOptions, avatarShapeOptions, avatarSizeOptions, avatarsInlineSizeOptions, badgeForButtonOptions, badgeIconSizeOptions, badgeSizeOptions, badgeVariantOptions, bannerVariants, breadCrumbSizeOptions, buttonCategoryOptions, buttonSizeOptions, buttonVariantOptions, colorThemes, columnOptions, datepickerWidthOptionsMap, defaultDateFormat, drawerVariants, dropdownAllowedAutoPlacements, dropdownItemVariantOptions, dropdownPlacements, dropdownVariantOptions, focusableTags, formInputWidths, formStateOptions, iconSizeOptions, iconVariantOptions, keyboard, labelColorOptions, linkVariantOptions, loadingIconSizes, loadingIconVariants, maxZIndex, modalButtonDefaults, modalSizeOptions, popoverPlacements, progressBarVariantOptions, resizeDebounceTime, tabsButtonDefaults, targetOptions, toggleLabelPosition, tokenVariants, tooltipActionEvents, tooltipDelay, tooltipPlacements, triggerVariantOptions, truncateOptions, variantCssColorMap, viewModeOptions };
|
|
322
|
+
export { COMMA, CONTRAST_LEVELS, HEX_REGEX, LEFT_MOUSE_BUTTON, alertVariantIconMap, alertVariantOptions, alignOptions, animatedIconVariantOptions, avatarShapeOptions, avatarSizeOptions, avatarsInlineSizeOptions, badgeForButtonOptions, badgeIconSizeOptions, badgeSizeOptions, badgeVariantOptions, bannerVariants, breadCrumbSizeOptions, buttonCategoryOptions, buttonSizeOptions, buttonVariantOptions, colorThemes, columnOptions, datepickerWidthOptionsMap, defaultDateFormat, drawerVariants, dropdownAllowedAutoPlacements, dropdownItemVariantOptions, dropdownPlacements, dropdownVariantOptions, focusableTags, formInputWidths, formStateOptions, iconSizeOptions, iconVariantOptions, isVue3, keyboard, labelColorOptions, linkVariantOptions, loadingIconSizes, loadingIconVariants, maxZIndex, modalButtonDefaults, modalSizeOptions, popoverPlacements, progressBarVariantOptions, resizeDebounceTime, tabsButtonDefaults, targetOptions, toggleLabelPosition, tokenVariants, tooltipActionEvents, tooltipDelay, tooltipPlacements, triggerVariantOptions, truncateOptions, variantCssColorMap, viewModeOptions };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Vue from 'vue';
|
|
2
|
+
import { isVue3 } from './constants';
|
|
2
3
|
|
|
3
4
|
// Fragment will be available only in Vue.js 3
|
|
4
5
|
const {
|
|
@@ -30,7 +31,6 @@ function isVnodeEmpty(vnode) {
|
|
|
30
31
|
}
|
|
31
32
|
function isSlotEmpty(vueInstance, slot, slotArgs) {
|
|
32
33
|
var _vueInstance$$scopedS, _vueInstance$$scopedS2;
|
|
33
|
-
const isVue3 = Boolean(Fragment);
|
|
34
34
|
const slotContent = isVue3 ?
|
|
35
35
|
// we need to check both $slots and $scopedSlots due to https://github.com/vuejs/core/issues/8869
|
|
36
36
|
// additionally, in @vue/compat $slot might be a function instead of array of vnodes (sigh)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitlab/ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "111.0.0",
|
|
4
4
|
"description": "GitLab UI Components",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -36,9 +36,8 @@
|
|
|
36
36
|
"build-tokens": "make tokens",
|
|
37
37
|
"build-migration-script": "esbuild --bundle --platform=node --target=esnext --outfile=bin/migrate_custom_utils_to_tw.bundled.mjs --format=esm --banner:js=\"import { createRequire as __gl__createRequire } from 'node:module'; const require = __gl__createRequire(import.meta.url);\" bin/migrate_custom_utils_to_tw.mjs",
|
|
38
38
|
"clean": "rm -r dist storybook",
|
|
39
|
-
"cy:
|
|
40
|
-
"cy:
|
|
41
|
-
"cy:run": "cypress run --browser firefox --env grepTags=-@a11y+-@storybook",
|
|
39
|
+
"cy:edge": "cypress run --browser edge --env grepTags=-@storybook",
|
|
40
|
+
"cy:run": "cypress run --browser firefox --env grepTags=-@storybook",
|
|
42
41
|
"start": "yarn storybook",
|
|
43
42
|
"storybook": "yarn storybook:prepare && storybook dev --ci --host ${STORYBOOK_HOST:-localhost} --port ${STORYBOOK_PORT:-9001} -c .storybook",
|
|
44
43
|
"storybook-vue3": "yarn storybook:prepare && VUE_VERSION=3 storybook dev --ci --host ${STORYBOOK_HOST:-localhost} --port ${STORYBOOK_PORT:-9001} -c .storybook",
|
|
@@ -49,7 +48,7 @@
|
|
|
49
48
|
"storybook:run": "npx http-server -bgs -p ${STORYBOOK_PORT:-9001} ./storybook",
|
|
50
49
|
"pretest:unit": "yarn build-tokens",
|
|
51
50
|
"test": "run-s test:unit test:visual",
|
|
52
|
-
"test:integration": "yarn run test:integration:server 'yarn cy:run && yarn cy:edge
|
|
51
|
+
"test:integration": "yarn run test:integration:server 'yarn cy:run && yarn cy:edge'",
|
|
53
52
|
"test:integration:server": "NODE_ENV=test start-test storybook:run http-get://${STORYBOOK_HOST:-localhost}:${STORYBOOK_PORT:-9001}/iframe.html",
|
|
54
53
|
"test:unit": "NODE_ENV=test jest",
|
|
55
54
|
"test:unit:watch": "yarn test:unit --watch",
|
|
@@ -140,12 +139,10 @@
|
|
|
140
139
|
"acorn": "^8.11.3",
|
|
141
140
|
"acorn-walk": "^8.3.2",
|
|
142
141
|
"autoprefixer": "^9.7.6",
|
|
143
|
-
"axe-core": "^4.2.3",
|
|
144
142
|
"axe-playwright": "^2.1.0",
|
|
145
143
|
"babel-jest": "29.0.1",
|
|
146
144
|
"babel-loader": "^8.0.5",
|
|
147
145
|
"cypress": "14.1.0",
|
|
148
|
-
"cypress-axe": "^1.4.0",
|
|
149
146
|
"cypress-real-events": "^1.11.0",
|
|
150
147
|
"dompurify": "^3.1.2",
|
|
151
148
|
"emoji-regex": "^10.0.0",
|
|
@@ -173,8 +170,8 @@
|
|
|
173
170
|
"npm-run-all": "^4.1.5",
|
|
174
171
|
"patch-package": "^8.0.0",
|
|
175
172
|
"pikaday": "^1.8.0",
|
|
176
|
-
"playwright": "^1.
|
|
177
|
-
"playwright-core": "^1.
|
|
173
|
+
"playwright": "^1.51.0",
|
|
174
|
+
"playwright-core": "^1.51.0",
|
|
178
175
|
"postcss": "8.4.28",
|
|
179
176
|
"postcss-loader": "^7.0.2",
|
|
180
177
|
"postcss-scss": "4.0.4",
|
|
@@ -208,11 +205,6 @@
|
|
|
208
205
|
"yargs": "^17.3.1",
|
|
209
206
|
"yarn-deduplicate": "^6.0.2"
|
|
210
207
|
},
|
|
211
|
-
"overrides": {
|
|
212
|
-
"cypress-axe": {
|
|
213
|
-
"axe-core": "4.2.3"
|
|
214
|
-
}
|
|
215
|
-
},
|
|
216
208
|
"release": {
|
|
217
209
|
"branches": [
|
|
218
210
|
"main"
|
|
@@ -18,6 +18,115 @@
|
|
|
18
18
|
- **Mention**: Indicates when a user is "@" mentioned in the content. The username links to the
|
|
19
19
|
user's profile. A mention link can be within body or meta content.
|
|
20
20
|
|
|
21
|
+
Use `<gl-link>` to render links. It can render standard `<a>` elements,
|
|
22
|
+
and also Vue Router and Nuxt links.
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<gl-link href="#foo">Link</gl-link>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Link type
|
|
29
|
+
|
|
30
|
+
By specifying a value in the `href` prop, a standard link (`<a>`) element will be rendered. To
|
|
31
|
+
generate a `<router-link>` instead, specify the route location via the `to` prop.
|
|
32
|
+
|
|
33
|
+
### Router links
|
|
34
|
+
|
|
35
|
+
Router links support various additional props.
|
|
36
|
+
|
|
37
|
+
If your app is running under [Nuxt.js](https://nuxtjs.org), the
|
|
38
|
+
[`<nuxt-link>`](https://nuxtjs.org/api/components-nuxt-link) component will be used instead of
|
|
39
|
+
`<router-link>`. The `<nuxt-link>` component supports all the same features as `<router-link>` (as
|
|
40
|
+
it is a wrapper component for `<router-link>`) and more.
|
|
41
|
+
|
|
42
|
+
#### `to`
|
|
43
|
+
|
|
44
|
+
- type: `string | Location`
|
|
45
|
+
- required to generate a `<router-link>`
|
|
46
|
+
|
|
47
|
+
Denotes the target route of the link. When clicked, the value of the `to` prop will be passed to
|
|
48
|
+
`router.push()` internally, so the value can be either a string or a location descriptor object.
|
|
49
|
+
|
|
50
|
+
```html
|
|
51
|
+
<!-- Literal string -->
|
|
52
|
+
<gl-link to="home">Home</gl-link>
|
|
53
|
+
<!-- Renders to -->
|
|
54
|
+
<a href="home">Home</a>
|
|
55
|
+
|
|
56
|
+
<!-- Omitting `v-bind` is fine, just as binding any other prop -->
|
|
57
|
+
<gl-link :to="'home'">Home</gl-link>
|
|
58
|
+
|
|
59
|
+
<!-- Same as above -->
|
|
60
|
+
<gl-link :to="{ path: 'home' }">Home</gl-link>
|
|
61
|
+
|
|
62
|
+
<!-- Named route -->
|
|
63
|
+
<gl-link :to="{ name: 'user', params: { userId: 123 } }">User</gl-link>
|
|
64
|
+
|
|
65
|
+
<!-- With query, resulting in `/register?plan=private` -->
|
|
66
|
+
<gl-link :to="{ path: 'register', query: { plan: 'private' } }">Register</gl-link>
|
|
67
|
+
|
|
68
|
+
<!-- Render a non-router link by omitting `to` and specifying an `href` -->
|
|
69
|
+
<gl-link href="/home">Home</gl-link>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### `replace`
|
|
73
|
+
|
|
74
|
+
- type: `boolean`
|
|
75
|
+
- default: `false`
|
|
76
|
+
|
|
77
|
+
Setting `replace` prop will call `router.replace()` instead of `router.push()` when clicked, so the
|
|
78
|
+
navigation will not leave a history record.
|
|
79
|
+
|
|
80
|
+
```html
|
|
81
|
+
<gl-link :to="{ path: '/abc'}" replace></gl-link>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
#### `active-class`
|
|
85
|
+
|
|
86
|
+
- type: `string`
|
|
87
|
+
- default: `'router-link-active'` (`'nuxt-link-active'` when using Nuxt.js)
|
|
88
|
+
|
|
89
|
+
Configure the active CSS class applied when the link is active. Note the default value can also be
|
|
90
|
+
configured globally via the `linkActiveClass`
|
|
91
|
+
[router constructor option](https://router.vuejs.org/api/#linkactiveclass).
|
|
92
|
+
|
|
93
|
+
With components that support router links (have a `to` prop), you will want to set this to the class
|
|
94
|
+
`'active'` (or a space separated string that includes `'active'`) to apply Bootstrap's active
|
|
95
|
+
styling on the component when the current route matches the `to` prop.
|
|
96
|
+
|
|
97
|
+
#### `exact-active-class`
|
|
98
|
+
|
|
99
|
+
- type: `string`
|
|
100
|
+
- default: `'router-link-exact-active'` (`'nuxt-link-exact-active'` when using Nuxt.js)
|
|
101
|
+
- availability: Vue Router 2.5.0+
|
|
102
|
+
|
|
103
|
+
Configure the active CSS class applied when the link is active with exact match. Note the default
|
|
104
|
+
value can also be configured globally via the `linkExactActiveClass`
|
|
105
|
+
[router constructor option](https://router.vuejs.org/api/#linkexactactiveclass).
|
|
106
|
+
|
|
107
|
+
With components that support router links (have a `to` prop), you will want to set this to the class
|
|
108
|
+
`'active'` (or a space separated string that includes `'active'`) to apply Bootstrap's active
|
|
109
|
+
styling on the component when the current route matches the `to` prop.
|
|
110
|
+
|
|
111
|
+
## Links with `href="#"`
|
|
112
|
+
|
|
113
|
+
Typically `<a href="#">` will cause the document to scroll to the top of page when clicked.
|
|
114
|
+
`<gl-link>` addresses this by preventing the default action (scroll to top) when `href` is set to
|
|
115
|
+
`#`.
|
|
116
|
+
|
|
117
|
+
If you need scroll to top behaviour, use a standard `<a href="#">...</a>` tag.
|
|
118
|
+
|
|
119
|
+
## Link disabled state
|
|
120
|
+
|
|
121
|
+
Disable link functionality by setting the `disabled` prop to true.
|
|
122
|
+
|
|
123
|
+
```html
|
|
124
|
+
<gl-link href="#foo" disabled>Disabled Link</gl-link>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Disabling a link handles stopping event propagation, preventing the default action from occurring,
|
|
128
|
+
and removing the link from the document tab sequence (`tabindex="-1"`).
|
|
129
|
+
|
|
21
130
|
## Security
|
|
22
131
|
|
|
23
132
|
This component implements a few security measures to make it as safe as possible by default.
|
|
@@ -1,17 +1,128 @@
|
|
|
1
1
|
<!-- eslint-disable vue/multi-word-component-names -->
|
|
2
2
|
<script>
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
3
|
+
import isFunction from 'lodash/isFunction';
|
|
4
|
+
import isString from 'lodash/isString';
|
|
5
|
+
import isObject from 'lodash/isObject';
|
|
6
|
+
import toString from 'lodash/toString';
|
|
7
|
+
import isBoolean from 'lodash/isBoolean';
|
|
8
|
+
import concat from 'lodash/concat';
|
|
9
|
+
import { SafeLinkDirective, isExternalURL } from '../../../directives/safe_link/safe_link';
|
|
10
|
+
import { stopEvent } from '../../../utils/utils';
|
|
11
|
+
import { isEvent } from '../../../vendor/bootstrap-vue/src/utils/inspect';
|
|
12
|
+
import { stringifyQueryObj } from '../../../vendor/bootstrap-vue/src/utils/router';
|
|
13
|
+
import { safeVueInstance } from '../../../vendor/bootstrap-vue/src/utils/safe-vue-instance';
|
|
14
|
+
import { attemptFocus, attemptBlur } from '../../../vendor/bootstrap-vue/src/utils/dom';
|
|
15
|
+
import { linkVariantOptions, isVue3 } from '../../../utils/constants';
|
|
16
|
+
|
|
17
|
+
const ANCHOR_TAG = 'a';
|
|
18
|
+
const NUXT_LINK_TAG = 'nuxt-link';
|
|
19
|
+
const VUE_ROUTER_LINK_TAG = 'router-link';
|
|
7
20
|
|
|
8
21
|
export default {
|
|
9
22
|
name: 'GlLink',
|
|
10
|
-
|
|
11
|
-
|
|
23
|
+
directives: {
|
|
24
|
+
SafeLink: SafeLinkDirective,
|
|
12
25
|
},
|
|
13
|
-
mixins: [SafeLinkMixin],
|
|
14
26
|
props: {
|
|
27
|
+
/**
|
|
28
|
+
* Denotes the target URL of the link for standard links.
|
|
29
|
+
*/
|
|
30
|
+
href: {
|
|
31
|
+
type: String,
|
|
32
|
+
required: false,
|
|
33
|
+
default: undefined,
|
|
34
|
+
},
|
|
35
|
+
/**
|
|
36
|
+
* When set to `true`, disables the component's functionality and places it in a disabled state.
|
|
37
|
+
*/
|
|
38
|
+
disabled: {
|
|
39
|
+
type: Boolean,
|
|
40
|
+
required: false,
|
|
41
|
+
default: false,
|
|
42
|
+
},
|
|
43
|
+
/**
|
|
44
|
+
* Skips sanitization of href if true. This should be used sparingly.
|
|
45
|
+
* Consult security team before setting to true.
|
|
46
|
+
*/
|
|
47
|
+
isUnsafeLink: {
|
|
48
|
+
type: Boolean,
|
|
49
|
+
required: false,
|
|
50
|
+
default: false,
|
|
51
|
+
},
|
|
52
|
+
/**
|
|
53
|
+
* Sets the 'rel' attribute on the rendered link.
|
|
54
|
+
*/
|
|
55
|
+
rel: {
|
|
56
|
+
type: String,
|
|
57
|
+
required: false,
|
|
58
|
+
default: null,
|
|
59
|
+
},
|
|
60
|
+
/**
|
|
61
|
+
* Sets the 'target' attribute on the rendered link.
|
|
62
|
+
*/
|
|
63
|
+
target: {
|
|
64
|
+
type: String,
|
|
65
|
+
required: false,
|
|
66
|
+
default: null,
|
|
67
|
+
},
|
|
68
|
+
/**
|
|
69
|
+
* When set to `true`, places the component in the active state with active styling
|
|
70
|
+
*/
|
|
71
|
+
active: {
|
|
72
|
+
type: Boolean,
|
|
73
|
+
required: false,
|
|
74
|
+
default: false,
|
|
75
|
+
},
|
|
76
|
+
/**
|
|
77
|
+
* <router-link> prop: Denotes the target route of the link.
|
|
78
|
+
* When clicked, the value of the to prop will be passed to `router.push()` internally,
|
|
79
|
+
* so the value can be either a string or a Location descriptor object.
|
|
80
|
+
*/
|
|
81
|
+
to: {
|
|
82
|
+
type: [Object, String],
|
|
83
|
+
required: false,
|
|
84
|
+
default: undefined,
|
|
85
|
+
},
|
|
86
|
+
/**
|
|
87
|
+
* <router-link> prop: Configure the active CSS class applied when the link is active.
|
|
88
|
+
*/
|
|
89
|
+
activeClass: {
|
|
90
|
+
type: String,
|
|
91
|
+
required: false,
|
|
92
|
+
default: undefined,
|
|
93
|
+
},
|
|
94
|
+
/**
|
|
95
|
+
* <router-link> prop: Configure the active CSS class applied when the link is active with exact match.
|
|
96
|
+
*/
|
|
97
|
+
exactActiveClass: {
|
|
98
|
+
type: String,
|
|
99
|
+
required: false,
|
|
100
|
+
default: undefined,
|
|
101
|
+
},
|
|
102
|
+
/**
|
|
103
|
+
* <router-link> prop: Setting the replace prop will call `router.replace()` instead of `router.push()`
|
|
104
|
+
* when clicked, so the navigation will not leave a history record.
|
|
105
|
+
*/
|
|
106
|
+
replace: {
|
|
107
|
+
type: Boolean,
|
|
108
|
+
required: false,
|
|
109
|
+
default: false,
|
|
110
|
+
},
|
|
111
|
+
/**
|
|
112
|
+
* <nuxt-link> prop: To improve the responsiveness of your Nuxt.js applications, when the link will be displayed within the viewport,
|
|
113
|
+
* Nuxt.js will automatically prefetch the code splitted page. Setting `prefetch` to `true` or `false` will overwrite the default value of `router.prefetchLinks`
|
|
114
|
+
*/
|
|
115
|
+
prefetch: {
|
|
116
|
+
type: Boolean,
|
|
117
|
+
required: false,
|
|
118
|
+
// Must be `null` to fall back to the value defined in the
|
|
119
|
+
// `nuxt.config.js` configuration file for `router.prefetchLinks`
|
|
120
|
+
// We convert `null` to `undefined`, so that Nuxt.js will use the
|
|
121
|
+
// compiled default
|
|
122
|
+
// Vue treats `undefined` as default of `false` for Boolean props,
|
|
123
|
+
// so we must set it as `null` here to be a true tri-state prop
|
|
124
|
+
default: null,
|
|
125
|
+
},
|
|
15
126
|
/**
|
|
16
127
|
* If inline variant, controls ↗ character visibility
|
|
17
128
|
*/
|
|
@@ -31,33 +142,187 @@ export default {
|
|
|
31
142
|
},
|
|
32
143
|
},
|
|
33
144
|
computed: {
|
|
145
|
+
safeLinkConfig() {
|
|
146
|
+
return {
|
|
147
|
+
skipSanitization: this.isUnsafeLink,
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
tag() {
|
|
151
|
+
const hasRouter = Boolean(safeVueInstance(this).$router);
|
|
152
|
+
const hasNuxt = Boolean(safeVueInstance(this).$nuxt);
|
|
153
|
+
|
|
154
|
+
if (!hasRouter || this.disabled || !this.to) {
|
|
155
|
+
return ANCHOR_TAG;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return hasNuxt ? NUXT_LINK_TAG : VUE_ROUTER_LINK_TAG;
|
|
159
|
+
},
|
|
160
|
+
isRouterLink() {
|
|
161
|
+
return this.tag !== ANCHOR_TAG;
|
|
162
|
+
},
|
|
163
|
+
isVue3RouterLink() {
|
|
164
|
+
return this.tag === VUE_ROUTER_LINK_TAG && isVue3;
|
|
165
|
+
},
|
|
34
166
|
isInlineAndHasExternalIcon() {
|
|
35
167
|
return (
|
|
36
168
|
this.showExternalIcon &&
|
|
37
169
|
this.variant === 'inline' &&
|
|
38
|
-
this
|
|
39
|
-
isExternalURL(this.target, this
|
|
170
|
+
this.href &&
|
|
171
|
+
isExternalURL(this.target, this.href)
|
|
40
172
|
);
|
|
41
173
|
},
|
|
42
|
-
|
|
174
|
+
computedHref() {
|
|
175
|
+
const fallback = '#';
|
|
176
|
+
const toFallback = '/';
|
|
177
|
+
const { to } = this;
|
|
178
|
+
|
|
179
|
+
// Return `href` when explicitly provided
|
|
180
|
+
if (this.href) {
|
|
181
|
+
return this.href;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (isString(to)) {
|
|
185
|
+
return to || toFallback;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Fallback to `to.path' + `to.query` + `to.hash` prop (if `to` is an object)
|
|
189
|
+
if (isObject(to) && (to.path || to.query || to.hash)) {
|
|
190
|
+
const path = toString(to.path);
|
|
191
|
+
const query = stringifyQueryObj(to.query);
|
|
192
|
+
let hash = toString(to.hash);
|
|
193
|
+
hash = !hash || hash.charAt(0) === '#' ? hash : `#${hash}`;
|
|
194
|
+
return `${path}${query}${hash}` || fallback;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return fallback;
|
|
198
|
+
},
|
|
199
|
+
computedProps() {
|
|
200
|
+
if (this.isRouterLink) {
|
|
201
|
+
return {
|
|
202
|
+
to: this.to,
|
|
203
|
+
activeClass: this.activeClass,
|
|
204
|
+
exactActiveClass: this.exactActiveClass,
|
|
205
|
+
replace: this.replace,
|
|
206
|
+
...(isBoolean(this.prefetch) ? { prefetch: this.prefetch } : {}),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
disabled: this.disabled,
|
|
212
|
+
...(this.disabled ? { 'aria-disabled': 'true', tabindex: '-1' } : {}),
|
|
213
|
+
rel: this.rel,
|
|
214
|
+
target: this.target,
|
|
215
|
+
href: this.computedHref,
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
computedListeners() {
|
|
219
|
+
const { click, ...listenersWithoutClick } = this.$listeners;
|
|
220
|
+
|
|
221
|
+
return listenersWithoutClick;
|
|
222
|
+
},
|
|
223
|
+
computedClass() {
|
|
43
224
|
return [
|
|
44
225
|
'gl-link',
|
|
45
226
|
linkVariantOptions[this.variant],
|
|
46
|
-
{
|
|
227
|
+
{
|
|
228
|
+
disabled: this.disabled,
|
|
229
|
+
active: this.active,
|
|
230
|
+
'gl-link-inline-external': this.isInlineAndHasExternalIcon,
|
|
231
|
+
},
|
|
47
232
|
];
|
|
48
233
|
},
|
|
49
234
|
},
|
|
235
|
+
methods: {
|
|
236
|
+
onClick(event, navigate) {
|
|
237
|
+
const eventIsEvent = isEvent(event);
|
|
238
|
+
const suppliedHandler = this.$listeners.click;
|
|
239
|
+
|
|
240
|
+
if (eventIsEvent && this.disabled) {
|
|
241
|
+
// Stop event from bubbling up
|
|
242
|
+
// Kill the event loop attached to this specific `EventTarget`
|
|
243
|
+
// Needed to prevent `vue-router` from navigating
|
|
244
|
+
stopEvent(event, { immediatePropagation: true });
|
|
245
|
+
} else {
|
|
246
|
+
// Router links do not emit instance `click` events, so we
|
|
247
|
+
// add in an `$emit('click', event)` on its Vue instance
|
|
248
|
+
//
|
|
249
|
+
// seems not to be required for Vue3 compat build
|
|
250
|
+
if (this.isRouterLink) {
|
|
251
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
252
|
+
event.currentTarget.__vue__?.$emit('click', event);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Call the suppliedHandler(s), if any provided
|
|
256
|
+
concat([], suppliedHandler)
|
|
257
|
+
.filter((h) => isFunction(h))
|
|
258
|
+
.forEach((handler) => {
|
|
259
|
+
// eslint-disable-next-line prefer-rest-params
|
|
260
|
+
handler(...arguments);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// this navigate function comes from Vue 3 router
|
|
264
|
+
// See https://router.vuejs.org/guide/advanced/extending-router-link.html#Extending-RouterLink
|
|
265
|
+
if (isFunction(navigate)) {
|
|
266
|
+
navigate(event);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// TODO: Remove deprecated 'clicked::link' event
|
|
270
|
+
this.$root.$emit('clicked::link', event);
|
|
271
|
+
}
|
|
272
|
+
// Stop scroll-to-top behavior or navigation on
|
|
273
|
+
// regular links when href is just '#'
|
|
274
|
+
if (eventIsEvent && !this.isRouterLink && this.computedHref === '#') {
|
|
275
|
+
stopEvent(event, { stopPropagation: false });
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
focus() {
|
|
279
|
+
attemptFocus(this.$el);
|
|
280
|
+
},
|
|
281
|
+
blur() {
|
|
282
|
+
attemptBlur(this.$el);
|
|
283
|
+
},
|
|
284
|
+
},
|
|
50
285
|
};
|
|
51
286
|
</script>
|
|
287
|
+
|
|
52
288
|
<template>
|
|
53
|
-
<
|
|
289
|
+
<component
|
|
290
|
+
:is="tag"
|
|
291
|
+
v-if="isVue3RouterLink"
|
|
292
|
+
#default="{ href: routerLinkHref, isActive, isExactActive, navigate }"
|
|
293
|
+
v-bind="computedProps"
|
|
294
|
+
custom
|
|
295
|
+
>
|
|
296
|
+
<a
|
|
297
|
+
v-safe-link:[safeLinkConfig]
|
|
298
|
+
:class="[computedClass, { [activeClass]: isActive, [exactActiveClass]: isExactActive }]"
|
|
299
|
+
:href="routerLinkHref"
|
|
300
|
+
v-on="computedListeners"
|
|
301
|
+
@click="onClick($event, navigate)"
|
|
302
|
+
>
|
|
303
|
+
<slot></slot>
|
|
304
|
+
</a>
|
|
305
|
+
</component>
|
|
306
|
+
<component
|
|
307
|
+
:is="tag"
|
|
308
|
+
v-else-if="isRouterLink"
|
|
309
|
+
v-safe-link:[safeLinkConfig]
|
|
310
|
+
v-bind="computedProps"
|
|
311
|
+
:class="computedClass"
|
|
312
|
+
v-on="computedListeners"
|
|
313
|
+
@click.native="onClick"
|
|
314
|
+
>
|
|
315
|
+
<slot></slot>
|
|
316
|
+
</component>
|
|
317
|
+
<component
|
|
318
|
+
:is="tag"
|
|
319
|
+
v-else
|
|
54
320
|
v-safe-link:[safeLinkConfig]
|
|
55
|
-
v-bind="
|
|
56
|
-
:
|
|
57
|
-
|
|
58
|
-
|
|
321
|
+
v-bind="computedProps"
|
|
322
|
+
:class="computedClass"
|
|
323
|
+
v-on="computedListeners"
|
|
324
|
+
@click="onClick"
|
|
59
325
|
>
|
|
60
|
-
<!-- @slot The link to display. -->
|
|
61
326
|
<slot></slot>
|
|
62
|
-
</
|
|
327
|
+
</component>
|
|
63
328
|
</template>
|
|
@@ -5,8 +5,12 @@ const getBaseURL = () => {
|
|
|
5
5
|
return `${protocol}//${host}`;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
const isTargetBlank = (target) => {
|
|
9
|
+
return target === '_blank';
|
|
10
|
+
};
|
|
11
|
+
|
|
8
12
|
export const isExternalURL = (target, hostname) => {
|
|
9
|
-
return target
|
|
13
|
+
return isTargetBlank(target) && hostname !== window.location.hostname;
|
|
10
14
|
};
|
|
11
15
|
|
|
12
16
|
const secureRel = (rel) => {
|
|
@@ -35,13 +39,13 @@ const transform = (el, { arg: { skipSanitization = false } = {} } = {}) => {
|
|
|
35
39
|
return;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
const { href, target, rel
|
|
42
|
+
const { href, target, rel } = el;
|
|
39
43
|
|
|
40
44
|
if (!isSafeURL(href)) {
|
|
41
45
|
el.href = 'about:blank';
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
if (
|
|
48
|
+
if (isTargetBlank(target)) {
|
|
45
49
|
el.rel = secureRel(rel);
|
|
46
50
|
}
|
|
47
51
|
};
|
package/src/utils/constants.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Vue from 'vue';
|
|
1
2
|
import { POSITION } from '../components/utilities/truncate/constants';
|
|
2
3
|
|
|
3
4
|
export const COMMA = ',';
|
|
@@ -372,3 +373,5 @@ export const loadingIconVariants = {
|
|
|
372
373
|
spinner: 'spinner',
|
|
373
374
|
dots: 'dots',
|
|
374
375
|
};
|
|
376
|
+
|
|
377
|
+
export const isVue3 = Boolean(Vue.Fragment);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Vue from 'vue';
|
|
2
|
+
import { isVue3 } from './constants';
|
|
2
3
|
|
|
3
4
|
// Fragment will be available only in Vue.js 3
|
|
4
5
|
const { Fragment, Comment, Text } = Vue;
|
|
@@ -32,8 +33,6 @@ export function isVnodeEmpty(vnode) {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export function isSlotEmpty(vueInstance, slot, slotArgs) {
|
|
35
|
-
const isVue3 = Boolean(Fragment);
|
|
36
|
-
|
|
37
36
|
const slotContent = isVue3
|
|
38
37
|
? // we need to check both $slots and $scopedSlots due to https://github.com/vuejs/core/issues/8869
|
|
39
38
|
// additionally, in @vue/compat $slot might be a function instead of array of vnodes (sigh)
|