@ncds/ui-admin 1.8.25 → 1.8.26-alpha.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/dist/cjs/assets/scripts/notification/FullWidthNotification.js +2 -0
- package/dist/cjs/assets/scripts/notification/MessageNotification.js +30 -23
- package/dist/cjs/assets/scripts/notification/const/classNames.js +3 -0
- package/dist/cjs/assets/scripts/notification/const/icons.js +11 -2
- package/dist/cjs/assets/scripts/notification/const/index.js +9 -4
- package/dist/cjs/assets/scripts/notification/const/sizes.js +6 -1
- package/dist/cjs/assets/scripts/notification/const/types.js +1 -10
- package/dist/cjs/assets/scripts/notification/messageTemplate.js +20 -8
- package/dist/cjs/assets/scripts/notification/utils.js +21 -17
- package/dist/cjs/constant/notification.js +15 -1
- package/dist/cjs/src/components/feedback-and-status/badge/Badge.js +1 -1
- package/dist/cjs/src/components/forms-and-input/combo-box/ComboBox.js +16 -126
- package/dist/cjs/src/components/forms-and-input/combo-box/__tests__/ComboBox.portal.test.js +3 -3
- package/dist/cjs/src/components/overlays/notification/FloatingNotification.js +4 -7
- package/dist/cjs/src/components/overlays/notification/FullWidthNotification.js +13 -8
- package/dist/cjs/src/components/overlays/notification/MessageNotification.js +20 -14
- package/dist/cjs/src/components/overlays/notification/MessageNotificationParts.js +22 -15
- package/dist/cjs/src/components/overlays/notification/Notification.js +6 -0
- package/dist/cjs/src/components/overlays/notification/NotificationActionButtons.js +27 -0
- package/dist/cjs/src/components/overlays/notification/__tests__/supportTextLink.test.js +58 -0
- package/dist/cjs/src/components/overlays/notification/linkTarget.js +18 -0
- package/dist/cjs/src/generated/scoped-css.js +1 -1
- package/dist/cjs/src/utils/index.js +11 -0
- package/dist/cjs/src/utils/url/__tests__/isSafeUrl.test.js +49 -0
- package/dist/cjs/src/utils/url/index.js +16 -0
- package/dist/cjs/src/utils/url/isSafeUrl.js +32 -0
- package/dist/esm/assets/scripts/notification/FullWidthNotification.js +3 -1
- package/dist/esm/assets/scripts/notification/MessageNotification.js +32 -25
- package/dist/esm/assets/scripts/notification/const/classNames.js +3 -0
- package/dist/esm/assets/scripts/notification/const/icons.js +10 -1
- package/dist/esm/assets/scripts/notification/const/index.js +2 -3
- package/dist/esm/assets/scripts/notification/const/sizes.js +5 -0
- package/dist/esm/assets/scripts/notification/const/types.js +1 -8
- package/dist/esm/assets/scripts/notification/messageTemplate.js +21 -9
- package/dist/esm/assets/scripts/notification/utils.js +19 -16
- package/dist/esm/constant/notification.js +14 -0
- package/dist/esm/src/components/feedback-and-status/badge/Badge.js +1 -1
- package/dist/esm/src/components/forms-and-input/combo-box/ComboBox.js +17 -127
- package/dist/esm/src/components/forms-and-input/combo-box/__tests__/ComboBox.portal.test.js +3 -3
- package/dist/esm/src/components/overlays/notification/FloatingNotification.js +4 -7
- package/dist/esm/src/components/overlays/notification/FullWidthNotification.js +13 -8
- package/dist/esm/src/components/overlays/notification/MessageNotification.js +21 -15
- package/dist/esm/src/components/overlays/notification/MessageNotificationParts.js +22 -15
- package/dist/esm/src/components/overlays/notification/Notification.js +6 -0
- package/dist/esm/src/components/overlays/notification/NotificationActionButtons.js +21 -0
- package/dist/esm/src/components/overlays/notification/__tests__/supportTextLink.test.js +55 -0
- package/dist/esm/src/components/overlays/notification/linkTarget.js +12 -0
- package/dist/esm/src/generated/scoped-css.js +1 -1
- package/dist/esm/src/utils/index.js +3 -1
- package/dist/esm/src/utils/url/__tests__/isSafeUrl.test.js +47 -0
- package/dist/esm/src/utils/url/index.js +1 -0
- package/dist/esm/src/utils/url/isSafeUrl.js +26 -0
- package/dist/temp/assets/scripts/notification/FullWidthNotification.js +3 -2
- package/dist/temp/assets/scripts/notification/MessageNotification.d.ts +2 -0
- package/dist/temp/assets/scripts/notification/MessageNotification.js +30 -25
- package/dist/temp/assets/scripts/notification/const/classNames.d.ts +3 -0
- package/dist/temp/assets/scripts/notification/const/classNames.js +3 -0
- package/dist/temp/assets/scripts/notification/const/icons.d.ts +6 -0
- package/dist/temp/assets/scripts/notification/const/icons.js +9 -0
- package/dist/temp/assets/scripts/notification/const/index.d.ts +3 -4
- package/dist/temp/assets/scripts/notification/const/index.js +2 -3
- package/dist/temp/assets/scripts/notification/const/sizes.d.ts +4 -0
- package/dist/temp/assets/scripts/notification/const/sizes.js +5 -0
- package/dist/temp/assets/scripts/notification/const/types.d.ts +28 -1
- package/dist/temp/assets/scripts/notification/const/types.js +1 -8
- package/dist/temp/assets/scripts/notification/messageTemplate.d.ts +5 -1
- package/dist/temp/assets/scripts/notification/messageTemplate.js +16 -11
- package/dist/temp/assets/scripts/notification/utils.d.ts +10 -3
- package/dist/temp/assets/scripts/notification/utils.js +21 -18
- package/dist/temp/constant/notification.d.ts +13 -0
- package/dist/temp/constant/notification.js +14 -0
- package/dist/temp/src/components/feedback-and-status/badge/Badge.d.ts +1 -1
- package/dist/temp/src/components/feedback-and-status/badge/Badge.js +1 -1
- package/dist/temp/src/components/forms-and-input/combo-box/ComboBox.d.ts +1 -1
- package/dist/temp/src/components/forms-and-input/combo-box/ComboBox.js +13 -119
- package/dist/temp/src/components/forms-and-input/combo-box/__tests__/ComboBox.portal.test.js +3 -3
- package/dist/temp/src/components/overlays/notification/FloatingNotification.js +2 -2
- package/dist/temp/src/components/overlays/notification/FullWidthNotification.d.ts +6 -1
- package/dist/temp/src/components/overlays/notification/FullWidthNotification.js +7 -2
- package/dist/temp/src/components/overlays/notification/MessageNotification.d.ts +15 -1
- package/dist/temp/src/components/overlays/notification/MessageNotification.js +11 -12
- package/dist/temp/src/components/overlays/notification/MessageNotificationParts.d.ts +10 -5
- package/dist/temp/src/components/overlays/notification/MessageNotificationParts.js +9 -6
- package/dist/temp/src/components/overlays/notification/Notification.d.ts +23 -2
- package/dist/temp/src/components/overlays/notification/Notification.js +3 -3
- package/dist/temp/src/components/overlays/notification/NotificationActionButtons.d.ts +12 -0
- package/dist/temp/src/components/overlays/notification/NotificationActionButtons.js +9 -0
- package/dist/temp/src/components/overlays/notification/__tests__/supportTextLink.test.js +73 -0
- package/dist/temp/src/components/overlays/notification/linkTarget.d.ts +11 -0
- package/dist/temp/src/components/overlays/notification/linkTarget.js +9 -0
- package/dist/temp/src/generated/scoped-css.js +1 -1
- package/dist/temp/src/utils/index.d.ts +1 -0
- package/dist/temp/src/utils/index.js +2 -0
- package/dist/temp/src/utils/url/__tests__/isSafeUrl.test.js +61 -0
- package/dist/temp/src/utils/url/index.d.ts +1 -0
- package/dist/temp/src/utils/url/index.js +1 -0
- package/dist/temp/src/utils/url/isSafeUrl.d.ts +8 -0
- package/dist/temp/src/utils/url/isSafeUrl.js +29 -0
- package/dist/types/assets/scripts/notification/MessageNotification.d.ts +2 -0
- package/dist/types/assets/scripts/notification/const/classNames.d.ts +3 -0
- package/dist/types/assets/scripts/notification/const/icons.d.ts +6 -0
- package/dist/types/assets/scripts/notification/const/index.d.ts +3 -4
- package/dist/types/assets/scripts/notification/const/sizes.d.ts +4 -0
- package/dist/types/assets/scripts/notification/const/types.d.ts +28 -1
- package/dist/types/assets/scripts/notification/messageTemplate.d.ts +5 -1
- package/dist/types/assets/scripts/notification/utils.d.ts +10 -3
- package/dist/types/constant/notification.d.ts +13 -0
- package/dist/types/src/components/feedback-and-status/badge/Badge.d.ts +1 -1
- package/dist/types/src/components/forms-and-input/combo-box/ComboBox.d.ts +1 -1
- package/dist/types/src/components/overlays/notification/FullWidthNotification.d.ts +6 -1
- package/dist/types/src/components/overlays/notification/MessageNotification.d.ts +15 -1
- package/dist/types/src/components/overlays/notification/MessageNotificationParts.d.ts +10 -5
- package/dist/types/src/components/overlays/notification/Notification.d.ts +23 -2
- package/dist/types/src/components/overlays/notification/NotificationActionButtons.d.ts +12 -0
- package/dist/types/src/components/overlays/notification/__tests__/supportTextLink.test.d.ts +1 -0
- package/dist/types/src/components/overlays/notification/linkTarget.d.ts +11 -0
- package/dist/types/src/utils/index.d.ts +1 -0
- package/dist/types/src/utils/url/__tests__/isSafeUrl.test.d.ts +1 -0
- package/dist/types/src/utils/url/index.d.ts +1 -0
- package/dist/types/src/utils/url/isSafeUrl.d.ts +8 -0
- package/dist/ui-admin/assets/styles/style.css +54 -34
- package/package.json +1 -1
- package/dist/cjs/src/components/forms-and-input/combo-box/__tests__/ComboBox.test.js +0 -455
- package/dist/esm/src/components/forms-and-input/combo-box/__tests__/ComboBox.test.js +0 -452
- package/dist/temp/src/components/forms-and-input/combo-box/__tests__/ComboBox.test.js +0 -293
- /package/dist/temp/src/components/{forms-and-input/combo-box/__tests__/ComboBox.test.d.ts → overlays/notification/__tests__/supportTextLink.test.d.ts} +0 -0
- /package/dist/{types/src/components/forms-and-input/combo-box/__tests__/ComboBox.test.d.ts → temp/src/utils/url/__tests__/isSafeUrl.test.d.ts} +0 -0
|
@@ -24,4 +24,15 @@ Object.keys(_dropdown).forEach(function (key) {
|
|
|
24
24
|
return _dropdown[key];
|
|
25
25
|
}
|
|
26
26
|
});
|
|
27
|
+
});
|
|
28
|
+
var _url = require("./url");
|
|
29
|
+
Object.keys(_url).forEach(function (key) {
|
|
30
|
+
if (key === "default" || key === "__esModule") return;
|
|
31
|
+
if (key in exports && exports[key] === _url[key]) return;
|
|
32
|
+
Object.defineProperty(exports, key, {
|
|
33
|
+
enumerable: true,
|
|
34
|
+
get: function () {
|
|
35
|
+
return _url[key];
|
|
36
|
+
}
|
|
37
|
+
});
|
|
27
38
|
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _vitest = require("vitest");
|
|
4
|
+
var _isSafeUrl = require("../isSafeUrl");
|
|
5
|
+
/**
|
|
6
|
+
* 알림 텍스트 링크(`supportTextLink`)의 href 허용 여부.
|
|
7
|
+
* React·Vanilla 가 이 함수 하나를 공유한다 (#538 리뷰 H2).
|
|
8
|
+
*
|
|
9
|
+
* 이 파일은 vitest 기본 환경(node)에서 돈다 — `window` 가 없는 SSR 상황 자체가 검증 대상이다.
|
|
10
|
+
*/
|
|
11
|
+
const SAFE = ['https://example.com/notice/1', 'http://example.com', 'HTTPS://EXAMPLE.COM', '/notice/1', 'notice/1', '../notice', '?page=2', '#section', ' https://example.com '];
|
|
12
|
+
const UNSAFE = ['javascript:alert(1)', 'JavaScript:alert(1)', ' JavaScript:alert(1)', '\tjavascript:alert(1)', 'java\nscript:alert(1)', 'data:text/html,<script>alert(1)</script>', 'DATA:text/html;base64,PHNjcmlwdD4=', 'vbscript:msgbox(1)', 'file:///etc/passwd', 'mailto:a@example.com', 'http://'];
|
|
13
|
+
(0, _vitest.describe)('isSafeUrl — window 없음(SSR)', () => {
|
|
14
|
+
(0, _vitest.it)('window 가 정의되지 않은 환경이다', () => {
|
|
15
|
+
(0, _vitest.expect)(typeof window).toBe('undefined');
|
|
16
|
+
});
|
|
17
|
+
_vitest.it.each(SAFE)('허용: %j', url => {
|
|
18
|
+
(0, _vitest.expect)((0, _isSafeUrl.isSafeUrl)(url)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
_vitest.it.each(UNSAFE)('차단: %j', url => {
|
|
21
|
+
(0, _vitest.expect)((0, _isSafeUrl.isSafeUrl)(url)).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
(0, _vitest.it)('빈 문자열은 차단한다', () => {
|
|
24
|
+
(0, _vitest.expect)((0, _isSafeUrl.isSafeUrl)('')).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
(0, _vitest.describe)('isSafeUrl — window 존재', () => {
|
|
28
|
+
(0, _vitest.afterEach)(() => {
|
|
29
|
+
_vitest.vi.unstubAllGlobals();
|
|
30
|
+
});
|
|
31
|
+
(0, _vitest.it)('현재 origin 기준으로 상대경로를 해석해 허용한다', () => {
|
|
32
|
+
_vitest.vi.stubGlobal('window', {
|
|
33
|
+
location: {
|
|
34
|
+
origin: 'https://admin.example.com'
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
(0, _vitest.expect)((0, _isSafeUrl.isSafeUrl)('/notice/1')).toBe(true);
|
|
38
|
+
(0, _vitest.expect)((0, _isSafeUrl.isSafeUrl)('javascript:alert(1)')).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
(0, _vitest.it)('origin 이 "null"(about:blank·sandbox iframe)이어도 상대경로를 허용한다', () => {
|
|
41
|
+
_vitest.vi.stubGlobal('window', {
|
|
42
|
+
location: {
|
|
43
|
+
origin: 'null'
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
(0, _vitest.expect)((0, _isSafeUrl.isSafeUrl)('/notice/1')).toBe(true);
|
|
47
|
+
(0, _vitest.expect)((0, _isSafeUrl.isSafeUrl)(' JavaScript:alert(1)')).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
var _isSafeUrl = require("./isSafeUrl");
|
|
7
|
+
Object.keys(_isSafeUrl).forEach(function (key) {
|
|
8
|
+
if (key === "default" || key === "__esModule") return;
|
|
9
|
+
if (key in exports && exports[key] === _isSafeUrl[key]) return;
|
|
10
|
+
Object.defineProperty(exports, key, {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
get: function () {
|
|
13
|
+
return _isSafeUrl[key];
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.isSafeUrl = isSafeUrl;
|
|
7
|
+
/**
|
|
8
|
+
* 상대경로 해석용 기본 origin. `window` 가 없거나(SSR) origin 이 불투명(`"null"` — about:blank,
|
|
9
|
+
* sandbox iframe)일 때 쓴다. 판정은 프로토콜만 보므로 실제 호스트와 무관하다.
|
|
10
|
+
*/
|
|
11
|
+
const FALLBACK_ORIGIN = 'http://localhost';
|
|
12
|
+
function resolveBaseOrigin() {
|
|
13
|
+
if (typeof window === 'undefined') return FALLBACK_ORIGIN;
|
|
14
|
+
const origin = window.location?.origin;
|
|
15
|
+
return origin && origin !== 'null' ? origin : FALLBACK_ORIGIN;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 링크 href 로 넣어도 안전한 URL 인지 판정한다. http/https 및 상대경로만 허용한다.
|
|
19
|
+
* javascript:, data:, file: 등 위험 프로토콜로 인한 XSS / 외부 페이로드 로딩을 차단.
|
|
20
|
+
*
|
|
21
|
+
* URL 파서가 앞뒤 공백·탭·개행 제거와 scheme 소문자화를 하므로 ` JavaScript:` 같은 우회도 막힌다.
|
|
22
|
+
* React(`MessageNotificationParts`·`FullWidthNotification`)와 Vanilla(`renderSupportingText`)가 공유한다.
|
|
23
|
+
*/
|
|
24
|
+
function isSafeUrl(url) {
|
|
25
|
+
if (!url.trim()) return false;
|
|
26
|
+
try {
|
|
27
|
+
const parsed = new URL(url, resolveBaseOrigin());
|
|
28
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { HIDE_PERMANENTLY_LABEL } from '../../../constant/notification';
|
|
2
2
|
import { sanitizeHtml } from '../utils/sanitize';
|
|
3
3
|
import { CLASS_NAMES, FULL_WIDTH_ICON_MAP, FULL_WIDTH_SIZES, SVG_ICONS } from './const';
|
|
4
|
-
import { bindNotificationEvents, createWrapperElement, renderActions, renderSupportingText, setupAutoClose } from './utils';
|
|
4
|
+
import { applySupportTextLinkTarget, bindNotificationEvents, createWrapperElement, renderActions, renderSupportingText, setupAutoClose } from './utils';
|
|
5
5
|
export class FullWidthNotification {
|
|
6
6
|
constructor(options) {
|
|
7
7
|
this.options = {
|
|
@@ -25,6 +25,7 @@ export class FullWidthNotification {
|
|
|
25
25
|
actions,
|
|
26
26
|
onClose,
|
|
27
27
|
supportTextLink,
|
|
28
|
+
supportTextLinkTarget,
|
|
28
29
|
onHidePermanently,
|
|
29
30
|
hidePermanentlyLabel
|
|
30
31
|
} = this.options;
|
|
@@ -40,6 +41,7 @@ export class FullWidthNotification {
|
|
|
40
41
|
onHidePermanently,
|
|
41
42
|
hidePermanentlyLabel
|
|
42
43
|
}));
|
|
44
|
+
applySupportTextLinkTarget(wrapper, CLASS_NAMES.FULL_WIDTH.TEXT_CONTAINER, CLASS_NAMES.FULL_WIDTH.LINK, supportTextLinkTarget);
|
|
43
45
|
return wrapper;
|
|
44
46
|
}
|
|
45
47
|
buildTemplate(params) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { FeaturedIcon } from '../featuredIcon';
|
|
2
2
|
import { sanitizeHtml } from '../utils/sanitize';
|
|
3
|
-
import { CLASS_NAMES,
|
|
3
|
+
import { CLASS_NAMES, MESSAGE_BLOCK_SIZES, MESSAGE_ICON_MAP, MESSAGE_SIZES } from './const';
|
|
4
4
|
import { buildMessageTemplate } from './messageTemplate';
|
|
5
|
-
import { bindNotificationEvents, createWrapperElement, setupAutoClose } from './utils';
|
|
5
|
+
import { applySupportTextLinkTarget, bindNotificationEvents, createWrapperElement, setupAutoClose } from './utils';
|
|
6
6
|
export class MessageNotification {
|
|
7
7
|
constructor(options) {
|
|
8
8
|
this.options = {
|
|
@@ -26,38 +26,28 @@ export class MessageNotification {
|
|
|
26
26
|
actions,
|
|
27
27
|
onClose,
|
|
28
28
|
onHidePermanently,
|
|
29
|
-
hidePermanentlyLabel
|
|
29
|
+
hidePermanentlyLabel,
|
|
30
|
+
variant,
|
|
31
|
+
supportTextLink,
|
|
32
|
+
supportTextLinkTarget
|
|
30
33
|
} = this.options;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const iconFunction = FLOATING_ICON_MAP[actualColor];
|
|
38
|
-
// FeaturedIcon 생성
|
|
39
|
-
let featuredIconElement = null;
|
|
40
|
-
if (iconFunction) {
|
|
41
|
-
const iconSvg = iconFunction(MESSAGE_SIZES.ICON_PIXEL);
|
|
42
|
-
this.featuredIcon = FeaturedIcon.create({
|
|
43
|
-
svgString: iconSvg,
|
|
44
|
-
theme: 'light-circle',
|
|
45
|
-
color: actualColor,
|
|
46
|
-
size: MESSAGE_SIZES.FEATURED_ICON,
|
|
47
|
-
// React 구현과 동일하게 `__icon`(flex-shrink: 0)을 부여한다.
|
|
48
|
-
// 누락 시 긴 타이틀에서 아이콘이 찌그러진다.
|
|
49
|
-
className: CLASS_NAMES.MESSAGE.ICON
|
|
50
|
-
});
|
|
51
|
-
featuredIconElement = this.featuredIcon.getElement();
|
|
52
|
-
}
|
|
34
|
+
const isBlock = variant === 'block';
|
|
35
|
+
// default 는 neutral, error, warning, success 4가지 색상만 지원 — info 는 block 에서만 유지한다.
|
|
36
|
+
const actualColor = !isBlock && color === 'info' ? 'neutral' : color;
|
|
37
|
+
const wrapperClassName = [isBlock ? CLASS_NAMES.MESSAGE.BLOCK : '', className].filter(Boolean).join(' ');
|
|
38
|
+
const wrapper = createWrapperElement(CLASS_NAMES.MESSAGE.BASE, actualColor, wrapperClassName);
|
|
39
|
+
const featuredIconElement = this.createFeaturedIcon(actualColor, isBlock);
|
|
53
40
|
wrapper.innerHTML = sanitizeHtml(buildMessageTemplate({
|
|
54
41
|
title,
|
|
55
42
|
supportingText,
|
|
43
|
+
supportTextLink: isBlock ? supportTextLink : undefined,
|
|
44
|
+
color: actualColor,
|
|
56
45
|
actions,
|
|
57
46
|
onClose,
|
|
58
47
|
onHidePermanently,
|
|
59
48
|
hidePermanentlyLabel
|
|
60
49
|
}));
|
|
50
|
+
applySupportTextLinkTarget(wrapper, CLASS_NAMES.MESSAGE.TEXT_CONTAINER, CLASS_NAMES.MESSAGE.LINK, supportTextLinkTarget);
|
|
61
51
|
// FeaturedIcon을 content-wrapper에 추가
|
|
62
52
|
if (featuredIconElement) {
|
|
63
53
|
const contentWrapper = wrapper.querySelector(`.${CLASS_NAMES.MESSAGE.CONTENT_WRAPPER}`);
|
|
@@ -67,6 +57,23 @@ export class MessageNotification {
|
|
|
67
57
|
}
|
|
68
58
|
return wrapper;
|
|
69
59
|
}
|
|
60
|
+
/** 색상별 FeaturedIcon. default 는 md(40/20), block 은 sm(32/16). */
|
|
61
|
+
createFeaturedIcon(color, isBlock) {
|
|
62
|
+
const iconFunction = MESSAGE_ICON_MAP[color];
|
|
63
|
+
if (!iconFunction) return null;
|
|
64
|
+
const sizes = isBlock ? MESSAGE_BLOCK_SIZES : MESSAGE_SIZES;
|
|
65
|
+
this.featuredIcon = FeaturedIcon.create({
|
|
66
|
+
svgString: iconFunction(sizes.ICON_PIXEL),
|
|
67
|
+
theme: 'light-circle',
|
|
68
|
+
// FeaturedIcon 에는 info 색이 없다. block+info 의 violet 톤은 message SCSS 가 덮어쓴다.
|
|
69
|
+
color: color === 'info' ? 'neutral' : color,
|
|
70
|
+
size: sizes.FEATURED_ICON,
|
|
71
|
+
// React 구현과 동일하게 `__icon`(flex-shrink: 0)을 부여한다.
|
|
72
|
+
// 누락 시 긴 타이틀에서 아이콘이 찌그러진다.
|
|
73
|
+
className: CLASS_NAMES.MESSAGE.ICON
|
|
74
|
+
});
|
|
75
|
+
return this.featuredIcon.getElement();
|
|
76
|
+
}
|
|
70
77
|
bindEvents() {
|
|
71
78
|
bindNotificationEvents(this.element, this.options.actions, this.options.onClose, () => this.remove());
|
|
72
79
|
// 다시보지 않기 버튼 이벤트 바인딩
|
|
@@ -9,6 +9,7 @@ export const CLASS_NAMES = {
|
|
|
9
9
|
TEXT_CONTAINER: 'ncua-full-width-notification__text-container',
|
|
10
10
|
TITLE: 'ncua-full-width-notification__title',
|
|
11
11
|
SUPPORTING_TEXT: 'ncua-full-width-notification__supporting-text',
|
|
12
|
+
LINK: 'ncua-full-width-notification__link',
|
|
12
13
|
ACTIONS_CONTAINER: 'ncua-full-width-notification__actions-container',
|
|
13
14
|
ACTIONS: 'ncua-full-width-notification__actions',
|
|
14
15
|
CLOSE_BUTTON: 'ncua-full-width-notification__close-button'
|
|
@@ -26,6 +27,7 @@ export const CLASS_NAMES = {
|
|
|
26
27
|
},
|
|
27
28
|
MESSAGE: {
|
|
28
29
|
BASE: 'ncua-message-notification',
|
|
30
|
+
BLOCK: 'ncua-message-notification--block',
|
|
29
31
|
CONTAINER: 'ncua-message-notification__container',
|
|
30
32
|
CONTENT: 'ncua-message-notification__content',
|
|
31
33
|
CONTENT_WRAPPER: 'ncua-message-notification__content-wrapper',
|
|
@@ -33,6 +35,7 @@ export const CLASS_NAMES = {
|
|
|
33
35
|
TEXT_CONTAINER: 'ncua-message-notification__text-container',
|
|
34
36
|
TITLE: 'ncua-message-notification__title',
|
|
35
37
|
SUPPORTING_TEXT: 'ncua-message-notification__supporting-text',
|
|
38
|
+
LINK: 'ncua-message-notification__link',
|
|
36
39
|
ACTIONS_CONTAINER: 'ncua-message-notification__actions-container',
|
|
37
40
|
ACTIONS: 'ncua-message-notification__actions',
|
|
38
41
|
// 다시 보지 않기 + 닫기 그룹 전용 래퍼. ACTIONS_CONTAINER를 재사용하면
|
|
@@ -22,4 +22,13 @@ export const FULL_WIDTH_ICON_MAP = {
|
|
|
22
22
|
success: SVG_ICONS['check-circle'],
|
|
23
23
|
info: SVG_ICONS['info-circle']
|
|
24
24
|
};
|
|
25
|
-
export const ICON_MAP = FULL_WIDTH_ICON_MAP;
|
|
25
|
+
export const ICON_MAP = FULL_WIDTH_ICON_MAP;
|
|
26
|
+
/**
|
|
27
|
+
* message 타입 아이콘. floating 과 같은 4색에 block 변형 전용 info 를 더한다.
|
|
28
|
+
* `SVG_ICONS['info-circle']`은 full-width 전용 class·color 가 박혀 있어 쓰지 않는다
|
|
29
|
+
* (`.ncua-full-width-notification__icon`은 모바일에서 숨겨진다).
|
|
30
|
+
*/
|
|
31
|
+
export const MESSAGE_ICON_MAP = {
|
|
32
|
+
...FLOATING_ICON_MAP,
|
|
33
|
+
info: size => `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" fill="none" viewBox="0 0 24 24" stroke="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 16v-4m0-4h.01M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10"></path></svg>`
|
|
34
|
+
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// Re-export all constants and types from separate files
|
|
2
2
|
export { CLASS_NAMES } from './classNames';
|
|
3
|
-
export { FLOATING_ICON_MAP, FULL_WIDTH_ICON_MAP, ICON_MAP, SVG_ICONS } from './icons';
|
|
4
|
-
export { CLOSE_BUTTON_SIZES, CLOSE_BUTTON_SVG_SIZES, FEATURED_ICON_SIZES, FULL_WIDTH_SIZES, getSizes, ICON_PIXEL_SIZES, MESSAGE_SIZES } from './sizes';
|
|
5
|
-
export { MESSAGE_CLOSE_ICON_COLORS } from './types';
|
|
3
|
+
export { FLOATING_ICON_MAP, FULL_WIDTH_ICON_MAP, ICON_MAP, MESSAGE_ICON_MAP, SVG_ICONS } from './icons';
|
|
4
|
+
export { CLOSE_BUTTON_SIZES, CLOSE_BUTTON_SVG_SIZES, FEATURED_ICON_SIZES, FULL_WIDTH_SIZES, getSizes, ICON_PIXEL_SIZES, MESSAGE_BLOCK_SIZES, MESSAGE_SIZES } from './sizes';
|
|
@@ -33,6 +33,11 @@ export const MESSAGE_SIZES = {
|
|
|
33
33
|
ICON_PIXEL: '20',
|
|
34
34
|
CLOSE_BUTTON: '20'
|
|
35
35
|
};
|
|
36
|
+
// Message 알림 Block 변형 사이즈. 'sm' = 32px 원 / 16px 아이콘. 닫기 버튼은 default 와 같다.
|
|
37
|
+
export const MESSAGE_BLOCK_SIZES = {
|
|
38
|
+
FEATURED_ICON: 'sm',
|
|
39
|
+
ICON_PIXEL: '16'
|
|
40
|
+
};
|
|
36
41
|
// 사이즈 유틸리티 함수들
|
|
37
42
|
export const getSizes = {
|
|
38
43
|
featuredIcon: isMobile => isMobile ? FEATURED_ICON_SIZES.MOBILE : FEATURED_ICON_SIZES.DESKTOP,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { HIDE_PERMANENTLY_LABEL } from '../../../constant/notification';
|
|
1
|
+
import { HIDE_PERMANENTLY_LABEL, MESSAGE_CLOSE_ICON_COLOR } from '../../../constant/notification';
|
|
2
2
|
import { CLASS_NAMES, MESSAGE_SIZES, SVG_ICONS } from './const';
|
|
3
3
|
import { renderActions, renderSupportingText } from './utils';
|
|
4
4
|
/**
|
|
5
5
|
* message 타입 액션 버튼 사이즈. TO-BE 규격 28px이며 React `<Button size="xs">`와 같다.
|
|
6
|
-
*
|
|
6
|
+
* `renderActions` 기본값도 'xs'(#535)지만 message 규격을 명시적으로 고정해 둔다.
|
|
7
7
|
*/
|
|
8
8
|
const ACTION_BUTTON_SIZE = 'xs';
|
|
9
9
|
/**
|
|
@@ -15,6 +15,8 @@ export function buildMessageTemplate(params) {
|
|
|
15
15
|
const {
|
|
16
16
|
title,
|
|
17
17
|
supportingText,
|
|
18
|
+
supportTextLink,
|
|
19
|
+
color,
|
|
18
20
|
actions,
|
|
19
21
|
onClose,
|
|
20
22
|
onHidePermanently,
|
|
@@ -26,14 +28,15 @@ export function buildMessageTemplate(params) {
|
|
|
26
28
|
<div class="${CLASS_NAMES.MESSAGE.CONTENT_WRAPPER}">
|
|
27
29
|
<div class="${CLASS_NAMES.MESSAGE.TEXT_CONTAINER}">
|
|
28
30
|
<span class="${CLASS_NAMES.MESSAGE.TITLE}">${title}</span>
|
|
29
|
-
${renderSupportingText(supportingText, CLASS_NAMES.MESSAGE.SUPPORTING_TEXT)}
|
|
31
|
+
${renderSupportingText(supportingText, CLASS_NAMES.MESSAGE.SUPPORTING_TEXT, supportTextLink, CLASS_NAMES.MESSAGE.LINK)}
|
|
30
32
|
</div>
|
|
31
33
|
</div>
|
|
32
34
|
${renderActionsContainer({
|
|
33
35
|
actions,
|
|
34
36
|
onClose,
|
|
35
37
|
onHidePermanently,
|
|
36
|
-
hidePermanentlyLabel
|
|
38
|
+
hidePermanentlyLabel,
|
|
39
|
+
color
|
|
37
40
|
})}
|
|
38
41
|
</div>
|
|
39
42
|
</div>
|
|
@@ -49,13 +52,15 @@ function renderActionsContainer(params) {
|
|
|
49
52
|
actions,
|
|
50
53
|
onClose,
|
|
51
54
|
onHidePermanently,
|
|
52
|
-
hidePermanentlyLabel
|
|
55
|
+
hidePermanentlyLabel,
|
|
56
|
+
color
|
|
53
57
|
} = params;
|
|
54
58
|
const actionsHtml = renderActions(actions ?? [], CLASS_NAMES.MESSAGE.ACTIONS, ACTION_BUTTON_SIZE);
|
|
55
59
|
const footerHtml = renderFooterContainer({
|
|
56
60
|
onClose,
|
|
57
61
|
onHidePermanently,
|
|
58
|
-
hidePermanentlyLabel
|
|
62
|
+
hidePermanentlyLabel,
|
|
63
|
+
color
|
|
59
64
|
});
|
|
60
65
|
if (!actionsHtml && !footerHtml) return '';
|
|
61
66
|
return `<div class="${CLASS_NAMES.MESSAGE.ACTIONS_CONTAINER}">${actionsHtml}${footerHtml}</div>`;
|
|
@@ -65,13 +70,14 @@ function renderFooterContainer(params) {
|
|
|
65
70
|
const {
|
|
66
71
|
onClose,
|
|
67
72
|
onHidePermanently,
|
|
68
|
-
hidePermanentlyLabel
|
|
73
|
+
hidePermanentlyLabel,
|
|
74
|
+
color
|
|
69
75
|
} = params;
|
|
70
76
|
if (!onHidePermanently && !onClose) return '';
|
|
71
77
|
return `
|
|
72
78
|
<div class="${CLASS_NAMES.MESSAGE.FOOTER_CONTAINER}">
|
|
73
79
|
${renderHidePermanentlyButton(onHidePermanently, hidePermanentlyLabel)}
|
|
74
|
-
${renderCloseButton(onClose)}
|
|
80
|
+
${renderCloseButton(onClose, color)}
|
|
75
81
|
</div>
|
|
76
82
|
`;
|
|
77
83
|
}
|
|
@@ -90,11 +96,17 @@ function renderHidePermanentlyButton(onHidePermanently, hidePermanentlyLabel) {
|
|
|
90
96
|
</button>
|
|
91
97
|
`;
|
|
92
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* 닫기(X) 버튼. 아이콘 색은 React `<XClose color>`와 같이 svg 의 `color` 속성으로 주고
|
|
101
|
+
* path 는 `stroke="currentColor"`로 상속받는다 — 두 구현이 같은 마크업을 낸다.
|
|
102
|
+
*/
|
|
93
103
|
function renderCloseButton(onClose) {
|
|
104
|
+
let color = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'neutral';
|
|
94
105
|
if (!onClose) return '';
|
|
106
|
+
const iconColor = MESSAGE_CLOSE_ICON_COLOR[color];
|
|
95
107
|
return `
|
|
96
108
|
<button type="button" class="${CLASS_NAMES.MESSAGE.CLOSE_BUTTON}" aria-label="알림 닫기">
|
|
97
|
-
${SVG_ICONS['x-close'](MESSAGE_SIZES.CLOSE_BUTTON).replace('
|
|
109
|
+
${SVG_ICONS['x-close'](MESSAGE_SIZES.CLOSE_BUTTON).replace('<svg ', `<svg color="${iconColor}" `)}
|
|
98
110
|
</button>
|
|
99
111
|
`;
|
|
100
112
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BREAKPOINT } from '../../../src/constant/breakpoint';
|
|
2
|
+
import { isSafeUrl } from '../../../src/utils/url';
|
|
2
3
|
import { CLASS_NAMES } from './const';
|
|
3
4
|
// 공통 유틸리티 함수들
|
|
4
5
|
export function createWrapperElement(baseClass, color, className) {
|
|
@@ -14,18 +15,6 @@ export function buildClassName(baseClass, color, className) {
|
|
|
14
15
|
}
|
|
15
16
|
return classes.join(' ');
|
|
16
17
|
}
|
|
17
|
-
/**
|
|
18
|
-
* http/https 프로토콜만 허용한다.
|
|
19
|
-
* javascript:, data:, file: 등 위험 프로토콜로 인한 XSS / 외부 페이로드 로딩을 차단.
|
|
20
|
-
*/
|
|
21
|
-
function isSafeUrl(url) {
|
|
22
|
-
try {
|
|
23
|
-
const parsed = new URL(url, window.location.origin);
|
|
24
|
-
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
25
|
-
} catch {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
18
|
/**
|
|
30
19
|
* HTML 속성 값에 안전하게 삽입하기 위한 최소 이스케이프.
|
|
31
20
|
* 본문(body)이 아닌 속성 컨텍스트에서의 따옴표 탈출(`" onclick="..."`)을 방지.
|
|
@@ -34,17 +23,32 @@ function escapeHtmlAttr(str) {
|
|
|
34
23
|
return str.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''');
|
|
35
24
|
}
|
|
36
25
|
export function renderSupportingText(supportingText, className, supportTextLink) {
|
|
26
|
+
let linkClassName = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : CLASS_NAMES.FULL_WIDTH.LINK;
|
|
37
27
|
if (!supportingText) return '';
|
|
38
28
|
// 본문(supportingText) 자체는 이 함수의 호출 결과가 wrapper.innerHTML에
|
|
39
29
|
// 들어가기 전에 DOMPurify로 sanitize되므로 별도 이스케이프하지 않는다.
|
|
40
30
|
// 이렇게 해야 <br>, <strong> 등 서식용 태그가 그대로 동작한다.
|
|
41
31
|
if (supportTextLink && isSafeUrl(supportTextLink)) {
|
|
42
32
|
const safeLink = escapeHtmlAttr(supportTextLink);
|
|
43
|
-
return `<a href="${safeLink}" class="
|
|
33
|
+
return `<a href="${safeLink}" class="${linkClassName}"><span class="${className}">${supportingText}</span></a>`;
|
|
44
34
|
}
|
|
45
35
|
// unsafe URL이거나 link가 없으면 링크 없이 텍스트만 렌더링
|
|
46
36
|
return `<span class="${className}">${supportingText}</span>`;
|
|
47
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* `supportTextLinkTarget`이 `'_blank'`이면 `renderSupportingText`가 만든 링크를 새 탭으로 연다.
|
|
40
|
+
*
|
|
41
|
+
* target 은 sanitize **이후** 우리가 만든 링크(텍스트 컨테이너의 직계 `<a>`)에만 지정한다.
|
|
42
|
+
* DOMPurify 에 target 을 전역 허용하면 사용자 입력 HTML 의 `<a target>`까지 rel 없이 통과하기 때문이다.
|
|
43
|
+
* 런타임 입력이므로 `'_blank'`가 아닌 값은 전부 `'_self'`(속성 미부여)로 취급한다.
|
|
44
|
+
*/
|
|
45
|
+
export function applySupportTextLinkTarget(root, textContainerClassName, linkClassName, target) {
|
|
46
|
+
if (target !== '_blank') return;
|
|
47
|
+
const link = root.querySelector(`.${textContainerClassName} > a.${linkClassName}`);
|
|
48
|
+
if (!link) return;
|
|
49
|
+
link.setAttribute('target', '_blank');
|
|
50
|
+
link.setAttribute('rel', 'noopener noreferrer');
|
|
51
|
+
}
|
|
48
52
|
/** 액션 버튼 색상을 지정하지 않았을 때의 기본값. React `MessageNotificationParts`와 같다. */
|
|
49
53
|
const DEFAULT_ACTION_HIERARCHY = 'text';
|
|
50
54
|
/**
|
|
@@ -76,11 +80,10 @@ function buildActionKey(action) {
|
|
|
76
80
|
/**
|
|
77
81
|
* 액션 버튼 그룹 HTML을 만든다.
|
|
78
82
|
*
|
|
79
|
-
* `size` 기본값 '
|
|
80
|
-
* message 타입만 TO-BE 규격(28px)에 맞춰 'xs'를 넘긴다 — React `<Button size="xs">`와 동일.
|
|
83
|
+
* `size` 기본값 'xs'는 디자인 기준(#535 — 모든 type 액션 버튼 xs)이며 React `<Button size="xs">`와 같다.
|
|
81
84
|
*/
|
|
82
85
|
export function renderActions(actions, wrapperClass) {
|
|
83
|
-
let size = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '
|
|
86
|
+
let size = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'xs';
|
|
84
87
|
// 액션이 없으면 빈 문자열 반환
|
|
85
88
|
if (!actions || actions.length === 0) {
|
|
86
89
|
return '';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { COLOR } from './color';
|
|
1
2
|
/**
|
|
2
3
|
* 다시보지 않기 버튼의 타입별 기본 문구.
|
|
3
4
|
*
|
|
@@ -12,4 +13,17 @@
|
|
|
12
13
|
export const HIDE_PERMANENTLY_LABEL = {
|
|
13
14
|
message: '다시 보지 않기',
|
|
14
15
|
fullWidth: '다시보지 않기'
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* message 타입 닫기(X) 아이콘의 color 별 색.
|
|
19
|
+
*
|
|
20
|
+
* React(`MessageNotification.tsx`)와 Vanilla(`messageTemplate.ts`)가 함께 참조하는 단일 출처다.
|
|
21
|
+
* `info`는 `variant: 'block'`에서만 렌더된다(default 는 neutral 로 폴백).
|
|
22
|
+
*/
|
|
23
|
+
export const MESSAGE_CLOSE_ICON_COLOR = {
|
|
24
|
+
neutral: COLOR.gray700,
|
|
25
|
+
error: COLOR.red500,
|
|
26
|
+
warning: COLOR.orange500,
|
|
27
|
+
success: COLOR.green600,
|
|
28
|
+
info: COLOR.violet600
|
|
15
29
|
};
|