@descope-ui/descope-image 0.0.1
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 +13 -0
- package/e2e/descope-image.spec.ts +59 -0
- package/package.json +32 -0
- package/project.json +17 -0
- package/src/component/ImageClass.js +140 -0
- package/src/component/helpers.js +55 -0
- package/src/component/index.js +5 -0
- package/src/theme.js +7 -0
- package/stories/apple.svg +10 -0
- package/stories/demo.jpg +0 -0
- package/stories/descope-image.stories.js +63 -0
- package/stories/game.png +0 -0
- package/stories/google.svg +25 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
|
|
4
|
+
|
|
5
|
+
## 0.0.1 (2025-05-28)
|
|
6
|
+
|
|
7
|
+
### Dependency Updates
|
|
8
|
+
|
|
9
|
+
* `e2e-utils` updated to version `0.0.1`
|
|
10
|
+
* `test-assets` updated to version `0.0.1`
|
|
11
|
+
* `@descope-ui/common` updated to version `0.0.12`
|
|
12
|
+
* `@descope-ui/theme-globals` updated to version `0.0.12`
|
|
13
|
+
# Changelog
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import { getStoryUrl, loopConfig, loopPresets } from 'e2e-utils';
|
|
3
|
+
|
|
4
|
+
const storyName = 'descope-image';
|
|
5
|
+
const componentName = 'descope-image';
|
|
6
|
+
|
|
7
|
+
const componentAttributes = {
|
|
8
|
+
'st-width': ['', '50px', '200px'],
|
|
9
|
+
'st-height': ['', '50px', '200px'],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
test.describe('theme', () => {
|
|
13
|
+
loopConfig(componentAttributes, (attr, value) => {
|
|
14
|
+
test.describe(`${attr}: ${value}`, () => {
|
|
15
|
+
test.beforeEach(async ({ page }) => {
|
|
16
|
+
await page.goto(getStoryUrl(storyName, { [attr]: value }), {
|
|
17
|
+
waitUntil: 'networkidle',
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
test('style', async ({ page }) => {
|
|
21
|
+
const component = page.locator(componentName);
|
|
22
|
+
|
|
23
|
+
expect(await component.screenshot()).toMatchSnapshot();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('render image for dark theme', async ({ page }) => {
|
|
29
|
+
await page.goto(getStoryUrl(storyName, {}, { theme: 'dark' }), {
|
|
30
|
+
waitUntil: 'networkidle',
|
|
31
|
+
});
|
|
32
|
+
const component = page.locator(componentName);
|
|
33
|
+
expect(await component.screenshot()).toMatchSnapshot();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test.describe('presets', () => {
|
|
38
|
+
loopPresets(
|
|
39
|
+
{
|
|
40
|
+
'portrait image': {
|
|
41
|
+
'st-width': '50px',
|
|
42
|
+
'st-height': '200px',
|
|
43
|
+
},
|
|
44
|
+
'landscape image': {
|
|
45
|
+
'st-width': '200px',
|
|
46
|
+
'st-height': '50px',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
(preset, name) => {
|
|
50
|
+
test(name, async ({ page }) => {
|
|
51
|
+
await page.goto(getStoryUrl(storyName, preset), {
|
|
52
|
+
waitUntil: 'networkidle',
|
|
53
|
+
});
|
|
54
|
+
const component = page.locator(componentName);
|
|
55
|
+
expect(await component.screenshot()).toMatchSnapshot();
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@descope-ui/descope-image",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"exports": {
|
|
5
|
+
".": {
|
|
6
|
+
"import": "./src/component/index.js"
|
|
7
|
+
},
|
|
8
|
+
"./theme": {
|
|
9
|
+
"import": "./src/theme.js"
|
|
10
|
+
},
|
|
11
|
+
"./class": {
|
|
12
|
+
"import": "./src/component/ImageClass.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@playwright/test": "1.38.1",
|
|
17
|
+
"e2e-utils": "0.0.1",
|
|
18
|
+
"test-assets": "0.0.1"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"dompurify": "^3.1.6",
|
|
22
|
+
"@descope-ui/common": "0.0.12",
|
|
23
|
+
"@descope-ui/theme-globals": "0.0.12"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"link-workspace-packages": false
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "echo 'No tests defined' && exit 0",
|
|
30
|
+
"test:e2e": "echo 'No e2e tests defined' && exit 0"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/project.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@descope-ui/descope-image",
|
|
3
|
+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "packages/web-components/components/descope-image/src",
|
|
5
|
+
"projectType": "library",
|
|
6
|
+
"targets": {
|
|
7
|
+
"version": {
|
|
8
|
+
"executor": "@jscutlery/semver:version",
|
|
9
|
+
"options": {
|
|
10
|
+
"trackDeps": true,
|
|
11
|
+
"push": false,
|
|
12
|
+
"preset": "conventional"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"tags": []
|
|
17
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/* eslint-disable no-use-before-define */
|
|
2
|
+
import {
|
|
3
|
+
createStyleMixin,
|
|
4
|
+
draggableMixin,
|
|
5
|
+
componentNameValidationMixin,
|
|
6
|
+
} from '@descope-ui/common/components-mixins';
|
|
7
|
+
import { createBaseClass } from '@descope-ui/common/base-classes';
|
|
8
|
+
import { compose } from '@descope-ui/common/utils';
|
|
9
|
+
import { getComponentName } from '@descope-ui/common/components-helpers';
|
|
10
|
+
import { createImage } from './helpers';
|
|
11
|
+
import { injectStyle } from '@descope-ui/common/components-helpers';
|
|
12
|
+
|
|
13
|
+
export const componentName = getComponentName('image');
|
|
14
|
+
|
|
15
|
+
const srcAttrs = ['src', 'src-dark'];
|
|
16
|
+
|
|
17
|
+
class RawImage extends createBaseClass({
|
|
18
|
+
componentName,
|
|
19
|
+
baseSelector: 'slot',
|
|
20
|
+
}) {
|
|
21
|
+
static get observedAttributes() {
|
|
22
|
+
return srcAttrs;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
constructor() {
|
|
26
|
+
super();
|
|
27
|
+
|
|
28
|
+
this.attachShadow({ mode: 'open' }).innerHTML = `
|
|
29
|
+
<slot></slot>
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
injectStyle(
|
|
33
|
+
`
|
|
34
|
+
:host {
|
|
35
|
+
display: inline-flex;
|
|
36
|
+
}
|
|
37
|
+
:host > slot {
|
|
38
|
+
width: 100%;
|
|
39
|
+
height: 100%;
|
|
40
|
+
box-sizing: border-box;
|
|
41
|
+
display: flex;
|
|
42
|
+
overflow: hidden;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
::slotted(*) {
|
|
46
|
+
width: 100%;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.hidden {
|
|
50
|
+
display: none;
|
|
51
|
+
}
|
|
52
|
+
`,
|
|
53
|
+
this,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
init() {
|
|
58
|
+
super.init?.();
|
|
59
|
+
this.toggleVisibility(this.src);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
onThemeChange() {
|
|
63
|
+
this.renderImage();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
toggleVisibility(isVisible) {
|
|
67
|
+
if (isVisible) {
|
|
68
|
+
this.classList.remove('hidden');
|
|
69
|
+
} else {
|
|
70
|
+
this.classList.add('hidden');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get legacySrc() {
|
|
75
|
+
return this.getAttribute('src');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get themeSrc() {
|
|
79
|
+
return this.getAttribute(`src-${this.currentThemeName}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get src() {
|
|
83
|
+
return this.themeSrc || this.legacySrc;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// in order to fill an SVG with `currentColor` override all of its `fill` and `path` nodes
|
|
87
|
+
// with the value from the `st-fill` attribute
|
|
88
|
+
// eslint-disable-next-line class-methods-use-this
|
|
89
|
+
updateFillColor(node) {
|
|
90
|
+
// set fill to root node and all its relevant selectors
|
|
91
|
+
const elementsToReplace = [node, ...node.querySelectorAll('*[fill]')];
|
|
92
|
+
|
|
93
|
+
elementsToReplace.forEach((ele) => {
|
|
94
|
+
ele.setAttribute(
|
|
95
|
+
'fill',
|
|
96
|
+
`var(${ImageClass.cssVarList.fill}, ${ele.getAttribute('fill') || "''"})`,
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
renderImage() {
|
|
102
|
+
this.toggleVisibility(this.src);
|
|
103
|
+
|
|
104
|
+
createImage(this.src).then((res) => {
|
|
105
|
+
this.innerHTML = '';
|
|
106
|
+
if (res) {
|
|
107
|
+
this.updateFillColor(res);
|
|
108
|
+
this.appendChild(res);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// render only when src attribute matches current theme
|
|
114
|
+
shouldRender(src) {
|
|
115
|
+
const srcVal = this.getAttribute(src);
|
|
116
|
+
return this.src === srcVal;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
attributeChangedCallback(attrName, oldValue, newValue) {
|
|
120
|
+
super.attributeChangedCallback?.(attrName, oldValue, newValue);
|
|
121
|
+
|
|
122
|
+
if (oldValue === newValue) return;
|
|
123
|
+
|
|
124
|
+
if (this.shouldRender(attrName)) {
|
|
125
|
+
this.renderImage();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const ImageClass = compose(
|
|
131
|
+
createStyleMixin({
|
|
132
|
+
mappings: {
|
|
133
|
+
fill: {},
|
|
134
|
+
height: { selector: () => ':host' },
|
|
135
|
+
width: { selector: () => ':host' },
|
|
136
|
+
},
|
|
137
|
+
}),
|
|
138
|
+
draggableMixin,
|
|
139
|
+
componentNameValidationMixin,
|
|
140
|
+
)(RawImage);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import DOMPurify from 'dompurify';
|
|
2
|
+
|
|
3
|
+
const getFileExtension = (path) => {
|
|
4
|
+
const match = path.match(/\.([0-9a-z]+)(?:[\\?#]|$)/i);
|
|
5
|
+
return match ? match[1] : null;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const base64Prefix = 'data:image/svg+xml;base64,';
|
|
9
|
+
|
|
10
|
+
const isBase64Svg = (src) => src.startsWith(base64Prefix);
|
|
11
|
+
|
|
12
|
+
const createImgEle = (src) => {
|
|
13
|
+
const ele = document.createElement('img');
|
|
14
|
+
ele.setAttribute('src', src);
|
|
15
|
+
return ele;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const createSvgEle = (text) => {
|
|
19
|
+
// we want to purify the SVG to avoid XSS attacks
|
|
20
|
+
const clean = DOMPurify.sanitize(text, {
|
|
21
|
+
USE_PROFILES: { svg: true, svgFilters: true },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const parser = new DOMParser();
|
|
25
|
+
const ele = parser
|
|
26
|
+
.parseFromString(clean, 'image/svg+xml')
|
|
27
|
+
.querySelector('svg');
|
|
28
|
+
return ele;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const createImage = async (src) => {
|
|
32
|
+
try {
|
|
33
|
+
let ele;
|
|
34
|
+
if (isBase64Svg(src)) {
|
|
35
|
+
// handle base64 source
|
|
36
|
+
const svgXml = atob(src.slice(base64Prefix.length));
|
|
37
|
+
ele = createSvgEle(svgXml);
|
|
38
|
+
} else if (getFileExtension(src) === 'svg') {
|
|
39
|
+
// handle urls
|
|
40
|
+
const fetchedSrc = await fetch(src);
|
|
41
|
+
const text = await fetchedSrc.text();
|
|
42
|
+
ele = createSvgEle(text);
|
|
43
|
+
} else {
|
|
44
|
+
// handle binary
|
|
45
|
+
ele = createImgEle(src);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ele.style.setProperty('max-width', '100%');
|
|
49
|
+
ele.style.setProperty('max-height', '100%');
|
|
50
|
+
|
|
51
|
+
return ele;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
};
|
package/src/theme.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
width="1.5em"
|
|
3
|
+
height="1.5em"
|
|
4
|
+
viewBox="0 0 800 1000"
|
|
5
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
6
|
+
fill="#000000"
|
|
7
|
+
data-icon="apple"
|
|
8
|
+
>
|
|
9
|
+
<path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z" />
|
|
10
|
+
</svg>
|
package/stories/demo.jpg
ADDED
|
Binary file
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { componentName } from '../src/component';
|
|
2
|
+
import {
|
|
3
|
+
customHeightControl,
|
|
4
|
+
customWidthControl,
|
|
5
|
+
fillControl,
|
|
6
|
+
} from '@descope-ui/common/sb-controls';
|
|
7
|
+
|
|
8
|
+
import demoImageLight from './demo.jpg';
|
|
9
|
+
import demoImageDark from './game.png';
|
|
10
|
+
import fromUrlLight from './google.svg?no-inline';
|
|
11
|
+
import fromUrlDark from './apple.svg?no-inline';
|
|
12
|
+
|
|
13
|
+
const images = {
|
|
14
|
+
fromUrlLight,
|
|
15
|
+
fromUrlDark,
|
|
16
|
+
demoImageLight,
|
|
17
|
+
demoImageDark,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const Template = ({
|
|
21
|
+
src,
|
|
22
|
+
srcDark,
|
|
23
|
+
fill,
|
|
24
|
+
'st-width': customWidth,
|
|
25
|
+
'st-height': customHeight,
|
|
26
|
+
}) => {
|
|
27
|
+
return `
|
|
28
|
+
<descope-image
|
|
29
|
+
src="${images[src]}"
|
|
30
|
+
src-dark="${images[srcDark]}"
|
|
31
|
+
${fill ? `st-fill="${fill}"` : ''}
|
|
32
|
+
st-height="${customHeight || ''}"
|
|
33
|
+
st-width="${customWidth || ''}"
|
|
34
|
+
></descope-image>
|
|
35
|
+
`;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default {
|
|
39
|
+
component: componentName,
|
|
40
|
+
title: 'descope-image',
|
|
41
|
+
argTypes: {
|
|
42
|
+
...fillControl,
|
|
43
|
+
src: {
|
|
44
|
+
options: Object.keys(images),
|
|
45
|
+
control: { type: 'select' },
|
|
46
|
+
},
|
|
47
|
+
'src-dark': {
|
|
48
|
+
options: Object.keys(images),
|
|
49
|
+
control: { type: 'select' },
|
|
50
|
+
},
|
|
51
|
+
'st-height': customHeightControl['st-host-height'],
|
|
52
|
+
'st-width': customWidthControl['st-host-width'],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const Default = Template.bind({});
|
|
57
|
+
|
|
58
|
+
Default.args = {
|
|
59
|
+
src: 'demoImageLight',
|
|
60
|
+
srcDark: 'demoImageDark',
|
|
61
|
+
'st-width': '',
|
|
62
|
+
'st-height': '',
|
|
63
|
+
};
|
package/stories/game.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
width="1.5em"
|
|
3
|
+
height="1.5em"
|
|
4
|
+
viewBox="0 0 20 20"
|
|
5
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
6
|
+
data-icon="google"
|
|
7
|
+
>
|
|
8
|
+
<path
|
|
9
|
+
d="m19.6 10.2274c0-.70912-.0636-1.39092-.1818-2.04552h-9.4182v3.86822h5.3818c-.2318 1.25-.9363 2.3091-1.9954 3.0182v2.5091h3.2318c1.8909-1.7409 2.9818-4.3046 2.9818-7.35z"
|
|
10
|
+
fill="#4285f4"
|
|
11
|
+
/>
|
|
12
|
+
<path
|
|
13
|
+
d="m10 19.9999c2.7 0 4.9636-.8955 6.6181-2.4227l-3.2318-2.5091c-.8954.6-2.0409.9545-3.3863.9545-2.6046 0-4.8091-1.7591-5.5955-4.1227h-3.3409v2.5909c1.6455 3.2682 5.0273 5.5091 8.9364 5.5091z"
|
|
14
|
+
fill="#34a853"
|
|
15
|
+
/>
|
|
16
|
+
<path
|
|
17
|
+
d="m4.4045 11.8999c-.2-.6-.3136-1.2409-.3136-1.89997 0-.6591.1136-1.3.3136-1.9v-2.5909h-3.3409c-.6772 1.35-1.0636 2.8773-1.0636 4.4909 0 1.61357.3864 3.14087 1.0636 4.49087z"
|
|
18
|
+
fill="#fbbc04"
|
|
19
|
+
/>
|
|
20
|
+
<path
|
|
21
|
+
d="m10 3.9773c1.4681 0 2.7863.5045 3.8227 1.4954l2.8682-2.8682c-1.7318-1.6136-3.9955-2.6045-6.6909-2.6045-3.9091 0-7.2909 2.2409-8.9364 5.5091l3.3409 2.5909c.7864-2.3636 2.9909-4.1227 5.5955-4.1227z"
|
|
22
|
+
fill="#e94235"
|
|
23
|
+
/>
|
|
24
|
+
<script>alert(1)</script>
|
|
25
|
+
</svg>
|