tosijs-ui 1.0.1 → 1.0.2
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/README.md +4 -2
- package/dist/iife.js +70 -60
- package/dist/iife.js.map +42 -42
- package/dist/index.js +15 -37
- package/dist/index.js.map +39 -39
- package/dist/version.d.ts +1 -1
- package/package.json +2 -2
- package/dist/ab-test.js +0 -116
- package/dist/babylon-3d.js +0 -292
- package/dist/bodymovin-player.js +0 -172
- package/dist/bp-loader.js +0 -26
- package/dist/carousel.js +0 -308
- package/dist/code-editor.js +0 -102
- package/dist/color-input.js +0 -112
- package/dist/data-table.js +0 -774
- package/dist/drag-and-drop.js +0 -386
- package/dist/editable-rect.js +0 -450
- package/dist/filter-builder.js +0 -468
- package/dist/float.js +0 -170
- package/dist/form.js +0 -466
- package/dist/gamepad.js +0 -115
- package/dist/icon-data.js +0 -308
- package/dist/icon-types.js +0 -1
- package/dist/icons.js +0 -374
- package/dist/index-iife.js +0 -4
- package/dist/live-example.js +0 -611
- package/dist/localize.js +0 -381
- package/dist/make-sorter.js +0 -119
- package/dist/make-sorter.test.d.ts +0 -1
- package/dist/make-sorter.test.js +0 -48
- package/dist/mapbox.js +0 -161
- package/dist/markdown-viewer.js +0 -173
- package/dist/match-shortcut.js +0 -13
- package/dist/match-shortcut.test.d.ts +0 -1
- package/dist/match-shortcut.test.js +0 -194
- package/dist/menu.js +0 -614
- package/dist/notifications.js +0 -308
- package/dist/password-strength.js +0 -302
- package/dist/playwright.config.d.ts +0 -9
- package/dist/playwright.config.js +0 -73
- package/dist/pop-float.js +0 -231
- package/dist/rating.js +0 -192
- package/dist/rich-text.js +0 -296
- package/dist/segmented.js +0 -298
- package/dist/select.js +0 -427
- package/dist/side-nav.js +0 -106
- package/dist/size-break.js +0 -118
- package/dist/sizer.js +0 -92
- package/dist/src/ab-test.d.ts +0 -14
- package/dist/src/babylon-3d.d.ts +0 -53
- package/dist/src/bodymovin-player.d.ts +0 -32
- package/dist/src/bp-loader.d.ts +0 -0
- package/dist/src/carousel.d.ts +0 -113
- package/dist/src/code-editor.d.ts +0 -27
- package/dist/src/color-input.d.ts +0 -41
- package/dist/src/data-table.d.ts +0 -79
- package/dist/src/drag-and-drop.d.ts +0 -2
- package/dist/src/editable-rect.d.ts +0 -97
- package/dist/src/filter-builder.d.ts +0 -64
- package/dist/src/float.d.ts +0 -18
- package/dist/src/form.d.ts +0 -68
- package/dist/src/gamepad.d.ts +0 -34
- package/dist/src/icon-data.d.ts +0 -309
- package/dist/src/icon-types.d.ts +0 -7
- package/dist/src/icons.d.ts +0 -17
- package/dist/src/index.d.ts +0 -37
- package/dist/src/live-example.d.ts +0 -51
- package/dist/src/localize.d.ts +0 -30
- package/dist/src/make-sorter.d.ts +0 -3
- package/dist/src/mapbox.d.ts +0 -24
- package/dist/src/markdown-viewer.d.ts +0 -15
- package/dist/src/match-shortcut.d.ts +0 -9
- package/dist/src/menu.d.ts +0 -60
- package/dist/src/notifications.d.ts +0 -106
- package/dist/src/password-strength.d.ts +0 -35
- package/dist/src/pop-float.d.ts +0 -10
- package/dist/src/rating.d.ts +0 -62
- package/dist/src/rich-text.d.ts +0 -28
- package/dist/src/segmented.d.ts +0 -80
- package/dist/src/select.d.ts +0 -43
- package/dist/src/side-nav.d.ts +0 -36
- package/dist/src/size-break.d.ts +0 -18
- package/dist/src/sizer.d.ts +0 -34
- package/dist/src/tab-selector.d.ts +0 -91
- package/dist/src/tag-list.d.ts +0 -37
- package/dist/src/track-drag.d.ts +0 -5
- package/dist/src/version.d.ts +0 -1
- package/dist/src/via-tag.d.ts +0 -2
- package/dist/tab-selector.js +0 -326
- package/dist/tag-list.js +0 -375
- package/dist/track-drag.js +0 -143
- package/dist/version.js +0 -1
- package/dist/via-tag.js +0 -102
package/dist/notifications.js
DELETED
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
/*#
|
|
2
|
-
|
|
3
|
-
# notifications
|
|
4
|
-
|
|
5
|
-
`XinNotification` provides a singleton custom `<xin-notification>` element that manages
|
|
6
|
-
a list of notifications.
|
|
7
|
-
|
|
8
|
-
The notifications are displayed most-recent first. If the notifications would take more than
|
|
9
|
-
half the height of the display, they are scrolled.
|
|
10
|
-
|
|
11
|
-
You can post a notification simply with `XinNotification.post()` or `postNotification()`.
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
interface NotificationSpec {
|
|
15
|
-
message: string
|
|
16
|
-
type?: 'success' | 'info' | 'log' | 'warn' | 'error' | 'progress' // default 'info'
|
|
17
|
-
icon?: SVGElement | string // defaults to an info icon
|
|
18
|
-
duration?: number
|
|
19
|
-
progress?: () => number // return percentage completion
|
|
20
|
-
close?: () => void
|
|
21
|
-
}
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
If you provide a `progress` callback (which is assumed to return a number from `0-100`, with
|
|
25
|
-
100+ indicating completion) then `XinNotification` will poll it every second until the
|
|
26
|
-
task completes or the notification is closed. Returning 100 or more will automatically close
|
|
27
|
-
the notification.
|
|
28
|
-
|
|
29
|
-
If you configure a notification's `type = "progress"` but don't provide a `progress` callback
|
|
30
|
-
then an indefinite `<progress>` element will be displayed.
|
|
31
|
-
|
|
32
|
-
If you provide a `close` callback, it will be fired if the user closes the notification.
|
|
33
|
-
|
|
34
|
-
`postNotification` returns a callback function that closes the note programmatically (e.g.
|
|
35
|
-
when an operation completes). This will *also* call any `close` callback function you
|
|
36
|
-
provided. (The progress demos in the example exercise this functionality.)
|
|
37
|
-
|
|
38
|
-
```js
|
|
39
|
-
const { postNotification, icons } = xinjsui
|
|
40
|
-
|
|
41
|
-
const form = preview.querySelector('xin-form')
|
|
42
|
-
const submit = preview.querySelector('.submit')
|
|
43
|
-
const closeButton = preview.querySelector('.close')
|
|
44
|
-
|
|
45
|
-
let close
|
|
46
|
-
|
|
47
|
-
form.submitCallback = (value, isValid) => {
|
|
48
|
-
if (!isValid) return
|
|
49
|
-
if (value.type.startsWith('progress')) {
|
|
50
|
-
startTime = Date.now()
|
|
51
|
-
const { message, duration, icon } = value
|
|
52
|
-
close = postNotification({
|
|
53
|
-
message,
|
|
54
|
-
type: 'progress',
|
|
55
|
-
icon,
|
|
56
|
-
progress: value.type === 'progress' ? () => (Date.now() - startTime) / (10 * duration) : undefined,
|
|
57
|
-
close: () => { postNotification(`${value.message} cancelled`) },
|
|
58
|
-
})
|
|
59
|
-
} else {
|
|
60
|
-
close = postNotification(value)
|
|
61
|
-
}
|
|
62
|
-
console.log(close)
|
|
63
|
-
closeButton.disabled = false
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
submit.addEventListener('click', form.submit)
|
|
67
|
-
closeButton.addEventListener('click', () => {
|
|
68
|
-
if (close) {
|
|
69
|
-
close()
|
|
70
|
-
}
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
postNotification({
|
|
74
|
-
message: 'Welcome to xinjs-ui notifications, this message will disappear in 2s',
|
|
75
|
-
duration: 2
|
|
76
|
-
})
|
|
77
|
-
```
|
|
78
|
-
```html
|
|
79
|
-
<xin-form>
|
|
80
|
-
<h3 slot="header">Notification Test</h3>
|
|
81
|
-
<xin-field caption="Message" key="message" type="string" value="This is a test…"></xin-field>
|
|
82
|
-
<xin-field caption="Type" key="type" value="info">
|
|
83
|
-
<xin-select slot="input"
|
|
84
|
-
options="error,warn,info,success,log,,progress,progress (indefinite)"
|
|
85
|
-
></xin-select>
|
|
86
|
-
</xin-field>
|
|
87
|
-
<xin-field caption="Icon" key="icon" value="info">
|
|
88
|
-
<xin-select slot="input"
|
|
89
|
-
options="info,bug,thumbsUp,thumbsDown,message"
|
|
90
|
-
></xin-select>
|
|
91
|
-
</xin-field>
|
|
92
|
-
<xin-field caption="Duration" key="duration" type="number" value="2"></xin-field>
|
|
93
|
-
<button slot="footer" class="close" disabled>Close Last Notification</button>
|
|
94
|
-
<span slot="footer" class="elastic"></span>
|
|
95
|
-
<button slot="footer" class="submit">Post Notification</button>
|
|
96
|
-
</xin-form>
|
|
97
|
-
```
|
|
98
|
-
```css
|
|
99
|
-
xin-form {
|
|
100
|
-
height: 100%;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
xin-form::part(content) {
|
|
104
|
-
display: flex;
|
|
105
|
-
flex-direction: column;
|
|
106
|
-
padding: 10px;
|
|
107
|
-
gap: 10px;
|
|
108
|
-
background: var(--background);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
xin-form::part(header),
|
|
112
|
-
xin-form::part(footer) {
|
|
113
|
-
background: #eee;
|
|
114
|
-
justify-content: center;
|
|
115
|
-
padding: 10px;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
xin-form h3 {
|
|
119
|
-
margin: 0;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
xin-form label {
|
|
123
|
-
display: grid;
|
|
124
|
-
grid-template-columns: 120px 1fr;
|
|
125
|
-
}
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
## `postNotification(spec: NotificationSpec | string)`
|
|
129
|
-
|
|
130
|
-
This is simply a wrapper for `XinNotification.post()`.
|
|
131
|
-
*/
|
|
132
|
-
import { Component, elements, vars } from 'xinjs';
|
|
133
|
-
import { icons } from './icons';
|
|
134
|
-
import { findHighestZ } from './track-drag';
|
|
135
|
-
const { div, button } = elements;
|
|
136
|
-
const COLOR_MAP = {
|
|
137
|
-
error: 'red',
|
|
138
|
-
warn: 'orange',
|
|
139
|
-
info: 'royalblue',
|
|
140
|
-
log: 'gray',
|
|
141
|
-
success: 'green',
|
|
142
|
-
progress: 'royalblue',
|
|
143
|
-
};
|
|
144
|
-
export class XinNotification extends Component {
|
|
145
|
-
static singleton;
|
|
146
|
-
static styleSpec = {
|
|
147
|
-
':host': {
|
|
148
|
-
_notificationSpacing: 8,
|
|
149
|
-
_notificationWidth: 360,
|
|
150
|
-
_notificationPadding: `${vars.notificationSpacing} ${vars.notificationSpacing50} ${vars.notificationSpacing} ${vars.notificationSpacing200}`,
|
|
151
|
-
_notificationBg: '#fafafa',
|
|
152
|
-
_notificationAccentColor: '#aaa',
|
|
153
|
-
_notificationTextColor: '#444',
|
|
154
|
-
_notificationIconSize: vars.notificationSpacing300,
|
|
155
|
-
_notificationButtonSize: 48,
|
|
156
|
-
_notificationBorderWidth: '3px 0 0',
|
|
157
|
-
_notificationBorderRadius: vars.notificationSpacing50,
|
|
158
|
-
position: 'fixed',
|
|
159
|
-
left: 0,
|
|
160
|
-
right: 0,
|
|
161
|
-
bottom: 0,
|
|
162
|
-
paddingBottom: vars.notificationSpacing,
|
|
163
|
-
width: vars.notificationWidth,
|
|
164
|
-
display: 'flex',
|
|
165
|
-
flexDirection: 'column-reverse',
|
|
166
|
-
margin: '0 auto',
|
|
167
|
-
gap: vars.notificationSpacing,
|
|
168
|
-
maxHeight: '50vh',
|
|
169
|
-
overflow: 'hidden auto',
|
|
170
|
-
boxShadow: 'none !important',
|
|
171
|
-
},
|
|
172
|
-
':host *': {
|
|
173
|
-
color: vars.notificationTextColor,
|
|
174
|
-
},
|
|
175
|
-
':host .note': {
|
|
176
|
-
display: 'grid',
|
|
177
|
-
background: vars.notificationBg,
|
|
178
|
-
padding: vars.notificationPadding,
|
|
179
|
-
gridTemplateColumns: `${vars.notificationIconSize} 1fr ${vars.notificationButtonSize}`,
|
|
180
|
-
gap: vars.notificationSpacing,
|
|
181
|
-
alignItems: 'center',
|
|
182
|
-
borderRadius: vars.notificationBorderRadius,
|
|
183
|
-
boxShadow: `0 2px 8px #0006, inset 0 0 0 2px ${vars.notificationAccentColor}`,
|
|
184
|
-
borderColor: vars.notificationAccentColor,
|
|
185
|
-
borderWidth: vars.notificationBorderWidth,
|
|
186
|
-
borderStyle: 'solid',
|
|
187
|
-
transition: '0.5s ease-in',
|
|
188
|
-
transitionProperty: 'margin, opacity',
|
|
189
|
-
zIndex: 1,
|
|
190
|
-
},
|
|
191
|
-
':host .note .icon': {
|
|
192
|
-
stroke: vars.notificationAccentColor,
|
|
193
|
-
},
|
|
194
|
-
':host .note button': {
|
|
195
|
-
display: 'flex',
|
|
196
|
-
lineHeight: vars.notificationButtonSize,
|
|
197
|
-
padding: 0,
|
|
198
|
-
margin: 0,
|
|
199
|
-
height: vars.notificationButtonSize,
|
|
200
|
-
width: vars.notificationButtonSize,
|
|
201
|
-
background: 'transparent',
|
|
202
|
-
alignItems: 'center',
|
|
203
|
-
justifyContent: 'center',
|
|
204
|
-
boxShadow: 'none',
|
|
205
|
-
border: 'none',
|
|
206
|
-
position: 'relative',
|
|
207
|
-
},
|
|
208
|
-
':host .note button:hover svg': {
|
|
209
|
-
stroke: vars.notificationAccentColor,
|
|
210
|
-
},
|
|
211
|
-
':host .note button:active svg': {
|
|
212
|
-
borderRadius: 99,
|
|
213
|
-
stroke: vars.notificationBg,
|
|
214
|
-
background: vars.notificationAccentColor,
|
|
215
|
-
padding: vars.spacing50,
|
|
216
|
-
},
|
|
217
|
-
':host .note svg': {
|
|
218
|
-
height: vars.notificationIconSize,
|
|
219
|
-
width: vars.notificationIconSize,
|
|
220
|
-
pointerEvents: 'none',
|
|
221
|
-
},
|
|
222
|
-
':host .message': {
|
|
223
|
-
display: 'flex',
|
|
224
|
-
flexDirection: 'column',
|
|
225
|
-
alignItems: 'center',
|
|
226
|
-
gap: vars.notificationSpacing,
|
|
227
|
-
},
|
|
228
|
-
':host .note.closing': {
|
|
229
|
-
opacity: 0,
|
|
230
|
-
zIndex: 0,
|
|
231
|
-
},
|
|
232
|
-
};
|
|
233
|
-
static removeNote(note) {
|
|
234
|
-
note.classList.add('closing');
|
|
235
|
-
note.style.marginBottom = -note.offsetHeight + 'px';
|
|
236
|
-
const remove = () => {
|
|
237
|
-
note.remove();
|
|
238
|
-
};
|
|
239
|
-
note.addEventListener('transitionend', remove);
|
|
240
|
-
setTimeout(remove, 1000);
|
|
241
|
-
}
|
|
242
|
-
static post(spec) {
|
|
243
|
-
const { message, duration, type, close, progress, icon } = Object.assign({ type: 'info', duration: -1 }, typeof spec === 'string' ? { message: spec } : spec);
|
|
244
|
-
if (!this.singleton) {
|
|
245
|
-
this.singleton = xinNotification();
|
|
246
|
-
}
|
|
247
|
-
const singleton = this.singleton;
|
|
248
|
-
document.body.append(singleton);
|
|
249
|
-
singleton.style.zIndex = String(findHighestZ() + 1);
|
|
250
|
-
const _notificationAccentColor = COLOR_MAP[type];
|
|
251
|
-
const progressBar = progress || type === 'progress' ? elements.progress() : {};
|
|
252
|
-
const closeCallback = () => {
|
|
253
|
-
if (close) {
|
|
254
|
-
close();
|
|
255
|
-
}
|
|
256
|
-
XinNotification.removeNote(note);
|
|
257
|
-
};
|
|
258
|
-
const iconElement = icon instanceof SVGElement
|
|
259
|
-
? icon
|
|
260
|
-
: icon
|
|
261
|
-
? icons[icon]({ class: 'icon' })
|
|
262
|
-
: icons.info({ class: 'icon' });
|
|
263
|
-
const note = div({
|
|
264
|
-
class: `note ${type}`,
|
|
265
|
-
style: {
|
|
266
|
-
_notificationAccentColor,
|
|
267
|
-
},
|
|
268
|
-
}, iconElement, div({ class: 'message' }, div(message), progressBar), button({
|
|
269
|
-
class: 'close',
|
|
270
|
-
title: 'close',
|
|
271
|
-
// we can't use onClick because this lives inside a shadowDOM
|
|
272
|
-
apply(elt) {
|
|
273
|
-
elt.addEventListener('click', closeCallback);
|
|
274
|
-
},
|
|
275
|
-
}, icons.x()));
|
|
276
|
-
singleton.shadowRoot.append(note);
|
|
277
|
-
if (progressBar instanceof HTMLProgressElement &&
|
|
278
|
-
progress instanceof Function) {
|
|
279
|
-
progressBar.setAttribute('max', String(100));
|
|
280
|
-
progressBar.value = progress();
|
|
281
|
-
const interval = setInterval(() => {
|
|
282
|
-
if (!singleton.shadowRoot.contains(note)) {
|
|
283
|
-
clearInterval(interval);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
const percentage = progress();
|
|
287
|
-
progressBar.value = percentage;
|
|
288
|
-
if (percentage >= 100) {
|
|
289
|
-
XinNotification.removeNote(note);
|
|
290
|
-
}
|
|
291
|
-
}, 1000);
|
|
292
|
-
}
|
|
293
|
-
if (duration > 0) {
|
|
294
|
-
setTimeout(() => {
|
|
295
|
-
XinNotification.removeNote(note);
|
|
296
|
-
}, duration * 1000);
|
|
297
|
-
}
|
|
298
|
-
note.scrollIntoView();
|
|
299
|
-
return closeCallback;
|
|
300
|
-
}
|
|
301
|
-
content = null;
|
|
302
|
-
}
|
|
303
|
-
export const xinNotification = XinNotification.elementCreator({
|
|
304
|
-
tag: 'xin-notification',
|
|
305
|
-
});
|
|
306
|
-
export function postNotification(spec) {
|
|
307
|
-
return XinNotification.post(spec);
|
|
308
|
-
}
|
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
/*#
|
|
2
|
-
# password strength
|
|
3
|
-
|
|
4
|
-
Just wrap it a `<xin-password-strength>` element around an `<input>`
|
|
5
|
-
and it will gauge its content strength as a password. It will also
|
|
6
|
-
let you **securely verify** that the password hasn't been breached.
|
|
7
|
-
|
|
8
|
-
```js
|
|
9
|
-
const { xinLocalized, localize } = xinjsui
|
|
10
|
-
|
|
11
|
-
const toggle = preview.querySelector('.toggle')
|
|
12
|
-
const icon = preview.querySelector('xin-icon')
|
|
13
|
-
const input = preview.querySelector('input')
|
|
14
|
-
const breach = preview.querySelector('.breach')
|
|
15
|
-
const output = preview.querySelector('.output')
|
|
16
|
-
const passwordStrength = preview.querySelector('xin-password-strength')
|
|
17
|
-
|
|
18
|
-
// Localization Example
|
|
19
|
-
passwordStrength.append(xinLocalized({
|
|
20
|
-
refString: 'Yes',
|
|
21
|
-
localeChanged () {
|
|
22
|
-
this.parentElement.strengthDescriptions = [
|
|
23
|
-
'unacceptable',
|
|
24
|
-
'very weak',
|
|
25
|
-
'weak',
|
|
26
|
-
'moderate',
|
|
27
|
-
'strong',
|
|
28
|
-
'very strong',
|
|
29
|
-
].map(localize)
|
|
30
|
-
this.parentElement.queueRender()
|
|
31
|
-
}
|
|
32
|
-
}))
|
|
33
|
-
|
|
34
|
-
toggle.addEventListener('click', () => {
|
|
35
|
-
if (icon.icon === 'eye') {
|
|
36
|
-
input.type = 'text'
|
|
37
|
-
icon.icon = 'eyeOff'
|
|
38
|
-
} else {
|
|
39
|
-
input.type = 'password'
|
|
40
|
-
icon.icon = 'eye'
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
breach.addEventListener('click', async () => {
|
|
45
|
-
preview.querySelector('xin-password-strength').isBreached().then(isBreached => {
|
|
46
|
-
output.textContent =
|
|
47
|
-
isBreached
|
|
48
|
-
? 'This password has been breached, look at console for details'
|
|
49
|
-
: 'Seems OK'
|
|
50
|
-
output.classList.toggle('breached', isBreached)
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
```
|
|
54
|
-
```html
|
|
55
|
-
<xin-password-strength>
|
|
56
|
-
<input class="password" type="password">
|
|
57
|
-
<button class="toggle">
|
|
58
|
-
<xin-icon icon="eye"></xin-icon>
|
|
59
|
-
</button>
|
|
60
|
-
</xin-password-strength>
|
|
61
|
-
|
|
62
|
-
<br><br>
|
|
63
|
-
<button class="breach">
|
|
64
|
-
<xin-localized>Check if breached</xin-localized>
|
|
65
|
-
</button>
|
|
66
|
-
<div class="output"></div>
|
|
67
|
-
```
|
|
68
|
-
```css
|
|
69
|
-
input.password {
|
|
70
|
-
box-shadow: inset 0 0 0 2px var(--indicator-color);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
.breached {
|
|
74
|
-
color: white;
|
|
75
|
-
background: red;
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
## Algorithm
|
|
80
|
-
|
|
81
|
-
The password is assessed to have a strength based on:
|
|
82
|
-
|
|
83
|
-
- **length** one point for at least `goodLength` characters long.
|
|
84
|
-
- **[a-z]** one point for containing a lowercase letter
|
|
85
|
-
- **[A-Z]** one point for containing an uppercase letter
|
|
86
|
-
- **[0-9]** one point for containing a numeric character
|
|
87
|
-
- **^[a-zA-Z0-9]]** one point for containing some other kind of character
|
|
88
|
-
|
|
89
|
-
A password smaller than `minLength` is an automatic `0`.
|
|
90
|
-
|
|
91
|
-
## Attributes
|
|
92
|
-
|
|
93
|
-
- `minLength` defaults to `8`
|
|
94
|
-
- `goodLength` defaults to `12`
|
|
95
|
-
- `indicatorColors` six HTML colors, separated by commas, defaults to `'#f00,#f40,#f80,#ef0,#8f0,#0d4'`
|
|
96
|
-
- `descriptionColors` six HTML colors, sepeated by commans, defaults to `'#000,#000,#000,#000,#000,#fff'`
|
|
97
|
-
|
|
98
|
-
## Properties
|
|
99
|
-
|
|
100
|
-
- `value`, `strength` is a number from 0 to 5
|
|
101
|
-
- `issues` is a structure which you can use to generate feedback
|
|
102
|
-
|
|
103
|
-
```
|
|
104
|
-
<xin-password-strength>.issues = {
|
|
105
|
-
tooShort: boolean,
|
|
106
|
-
short: boolean,
|
|
107
|
-
noUpper: boolean,
|
|
108
|
-
noLower: boolean,
|
|
109
|
-
noNumber: boolean,
|
|
110
|
-
noSpecial: boolean,
|
|
111
|
-
}
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
## Customizing / Localizing Strings
|
|
115
|
-
|
|
116
|
-
The following properties control the feedback generated.
|
|
117
|
-
|
|
118
|
-
```
|
|
119
|
-
issueDescriptions = {
|
|
120
|
-
tooShort: 'too short',
|
|
121
|
-
short: 'short',
|
|
122
|
-
noUpper: 'no upper case',
|
|
123
|
-
noLower: 'no lower case',
|
|
124
|
-
noNumber: 'no digits',
|
|
125
|
-
noSpecial: 'no unusual characters',
|
|
126
|
-
}
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
```
|
|
130
|
-
strengthDescriptions = [
|
|
131
|
-
'unacceptable',
|
|
132
|
-
'very weak',
|
|
133
|
-
'weak',
|
|
134
|
-
'moderate',
|
|
135
|
-
'strong',
|
|
136
|
-
'very strong',
|
|
137
|
-
]
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
## `isBreached()`
|
|
141
|
-
|
|
142
|
-
`<xin-password-meter>` also provides an `isBreached(): Promise<boolean>` method
|
|
143
|
-
which uses [weakpass.com's API](https://weakpass.com/) to tell you if the password has been
|
|
144
|
-
breached.
|
|
145
|
-
|
|
146
|
-
> Note that `isBreached` does not send the plain-text password anywhere. It uses **SHA-1**
|
|
147
|
-
to hash the password and then sends that for lookup.
|
|
148
|
-
|
|
149
|
-
## Utility Functions
|
|
150
|
-
|
|
151
|
-
Two functions used internally for querying [Weakpass,com](https://weakpass.com/) are
|
|
152
|
-
provided in case they're useful on their own.
|
|
153
|
-
|
|
154
|
-
`isBreached(password: striing): Promise<boolean>` will return `true` if the password is
|
|
155
|
-
found in Weakpass's database (and spit out extra info to the console).
|
|
156
|
-
|
|
157
|
-
`digest(s: string, method="sha-1"): Promise<string>` is just a nice wrapper for `crypto.digest`.
|
|
158
|
-
*/
|
|
159
|
-
import { Component, elements, vars, varDefault } from 'xinjs';
|
|
160
|
-
export const digest = async (s, method = 'SHA-1') => {
|
|
161
|
-
// Convert password to an ArrayBuffer
|
|
162
|
-
const encoder = new TextEncoder();
|
|
163
|
-
const data = encoder.encode(s);
|
|
164
|
-
// Hash the password using SHA-1
|
|
165
|
-
const hashBuffer = await crypto.subtle.digest(method, data);
|
|
166
|
-
// Convert the hash to a hexadecimal string
|
|
167
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
168
|
-
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
169
|
-
return hashHex;
|
|
170
|
-
};
|
|
171
|
-
export const isBreached = async (password) => {
|
|
172
|
-
const hashHex = await digest(password);
|
|
173
|
-
const response = await fetch(`https://weakpass.com/api/v1/search/${hashHex}`);
|
|
174
|
-
if (response.ok) {
|
|
175
|
-
const result = await response.json();
|
|
176
|
-
console.log('password found in weakpass database', result);
|
|
177
|
-
}
|
|
178
|
-
return response.status !== 404;
|
|
179
|
-
};
|
|
180
|
-
const { span, xinSlot } = elements;
|
|
181
|
-
export class XinPasswordStrength extends Component {
|
|
182
|
-
minLength = 8;
|
|
183
|
-
goodLength = 12;
|
|
184
|
-
indicatorColors = '#f00,#f40,#f80,#ef0,#8f0,#0a2';
|
|
185
|
-
descriptionColors = '#000,#000,#000,#000,#000,#fff';
|
|
186
|
-
issues = {
|
|
187
|
-
tooShort: true,
|
|
188
|
-
short: true,
|
|
189
|
-
noUpper: true,
|
|
190
|
-
noLower: true,
|
|
191
|
-
noNumber: true,
|
|
192
|
-
noSpecial: true,
|
|
193
|
-
};
|
|
194
|
-
issueDescriptions = {
|
|
195
|
-
tooShort: 'too short',
|
|
196
|
-
short: 'short',
|
|
197
|
-
noUpper: 'no upper case',
|
|
198
|
-
noLower: 'no lower case',
|
|
199
|
-
noNumber: 'no digits',
|
|
200
|
-
noSpecial: 'no unusual characters',
|
|
201
|
-
};
|
|
202
|
-
value = 0;
|
|
203
|
-
strengthDescriptions = [
|
|
204
|
-
'unacceptable',
|
|
205
|
-
'very weak',
|
|
206
|
-
'weak',
|
|
207
|
-
'moderate',
|
|
208
|
-
'strong',
|
|
209
|
-
'very strong',
|
|
210
|
-
];
|
|
211
|
-
constructor() {
|
|
212
|
-
super();
|
|
213
|
-
this.initAttributes('minLength', 'goodLength', 'indicatorColors');
|
|
214
|
-
}
|
|
215
|
-
strength(password) {
|
|
216
|
-
this.issues = {
|
|
217
|
-
tooShort: password.length < this.minLength,
|
|
218
|
-
short: password.length < this.goodLength,
|
|
219
|
-
noUpper: !password.match(/[A-Z]/),
|
|
220
|
-
noLower: !password.match(/[a-z]/),
|
|
221
|
-
noNumber: !password.match(/[0-9]/),
|
|
222
|
-
noSpecial: !password.match(/[^a-zA-Z0-9]/),
|
|
223
|
-
};
|
|
224
|
-
return this.issues.tooShort
|
|
225
|
-
? 0
|
|
226
|
-
: Object.values(this.issues).filter((v) => !v).length - 1;
|
|
227
|
-
}
|
|
228
|
-
async isBreached() {
|
|
229
|
-
const password = this.querySelector('input')?.value;
|
|
230
|
-
if (!password || typeof password !== 'string') {
|
|
231
|
-
return true;
|
|
232
|
-
}
|
|
233
|
-
return await isBreached(password);
|
|
234
|
-
}
|
|
235
|
-
updateIndicator = (password) => {
|
|
236
|
-
const { level, description } = this.parts;
|
|
237
|
-
const colors = this.indicatorColors.split(',');
|
|
238
|
-
const descriptionColors = this.descriptionColors.split(',');
|
|
239
|
-
const strength = this.strength(password);
|
|
240
|
-
if (this.value !== strength) {
|
|
241
|
-
this.value = strength;
|
|
242
|
-
this.dispatchEvent(new Event('change'));
|
|
243
|
-
}
|
|
244
|
-
level.style.width = `${(strength + 1) * 16.67}%`;
|
|
245
|
-
this.style.setProperty('--indicator-color', colors[strength]);
|
|
246
|
-
this.style.setProperty('--description-color', descriptionColors[strength]);
|
|
247
|
-
description.textContent = this.strengthDescriptions[strength];
|
|
248
|
-
};
|
|
249
|
-
update = (event) => {
|
|
250
|
-
const input = event.target.closest('input');
|
|
251
|
-
this.updateIndicator(input?.value || '');
|
|
252
|
-
};
|
|
253
|
-
content = () => [
|
|
254
|
-
xinSlot({ onInput: this.update }),
|
|
255
|
-
span({ part: 'meter' }, span({ part: 'level' }), span({ part: 'description' })),
|
|
256
|
-
];
|
|
257
|
-
render() {
|
|
258
|
-
super.render();
|
|
259
|
-
const input = this.querySelector('input');
|
|
260
|
-
this.updateIndicator(input?.value);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
export const xinPasswordStrength = XinPasswordStrength.elementCreator({
|
|
264
|
-
tag: 'xin-password-strength',
|
|
265
|
-
styleSpec: {
|
|
266
|
-
':host': {
|
|
267
|
-
display: 'inline-flex',
|
|
268
|
-
flexDirection: 'column',
|
|
269
|
-
gap: vars.spacing50,
|
|
270
|
-
position: 'relative',
|
|
271
|
-
},
|
|
272
|
-
':host xin-slot': {
|
|
273
|
-
display: 'flex',
|
|
274
|
-
},
|
|
275
|
-
':host [part="meter"]': {
|
|
276
|
-
display: 'block',
|
|
277
|
-
position: 'relative',
|
|
278
|
-
height: varDefault.meterHeight('24px'),
|
|
279
|
-
background: varDefault.indicatorBg('white'),
|
|
280
|
-
borderRadius: varDefault.meterRadius('4px'),
|
|
281
|
-
boxShadow: varDefault.meterShadow(`inset 0 0 0 2px ${vars.indicatorColor}`),
|
|
282
|
-
},
|
|
283
|
-
':host [part="level"]': {
|
|
284
|
-
height: varDefault.levelHeight('20px'),
|
|
285
|
-
content: '" "',
|
|
286
|
-
display: 'inline-block',
|
|
287
|
-
width: 0,
|
|
288
|
-
transition: '0.15s ease-out',
|
|
289
|
-
background: vars.indicatorColor,
|
|
290
|
-
margin: varDefault.levelMargin('2px'),
|
|
291
|
-
borderRadius: varDefault.levelRadius('2px'),
|
|
292
|
-
},
|
|
293
|
-
':host [part="description"]': {
|
|
294
|
-
position: 'absolute',
|
|
295
|
-
inset: '0',
|
|
296
|
-
color: vars.descriptionColor,
|
|
297
|
-
height: varDefault.meterHeight('24px'),
|
|
298
|
-
lineHeight: varDefault.meterHeight('24px'),
|
|
299
|
-
textAlign: 'center',
|
|
300
|
-
},
|
|
301
|
-
},
|
|
302
|
-
});
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Read environment variables from file.
|
|
3
|
-
* https://github.com/motdotla/dotenv
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* See https://playwright.dev/docs/test-configuration.
|
|
7
|
-
*/
|
|
8
|
-
declare const _default: import("@playwright/test").PlaywrightTestConfig<{}, {}>;
|
|
9
|
-
export default _default;
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
-
/**
|
|
3
|
-
* Read environment variables from file.
|
|
4
|
-
* https://github.com/motdotla/dotenv
|
|
5
|
-
*/
|
|
6
|
-
// import dotenv from 'dotenv';
|
|
7
|
-
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
|
8
|
-
/**
|
|
9
|
-
* See https://playwright.dev/docs/test-configuration.
|
|
10
|
-
*/
|
|
11
|
-
export default defineConfig({
|
|
12
|
-
testDir: './tests',
|
|
13
|
-
/* Run tests in files in parallel */
|
|
14
|
-
fullyParallel: true,
|
|
15
|
-
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
|
16
|
-
forbidOnly: !!process.env.CI,
|
|
17
|
-
/* Retry on CI only */
|
|
18
|
-
retries: process.env.CI ? 2 : 0,
|
|
19
|
-
/* Opt out of parallel tests on CI. */
|
|
20
|
-
workers: process.env.CI ? 1 : undefined,
|
|
21
|
-
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
|
22
|
-
reporter: 'html',
|
|
23
|
-
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
24
|
-
/* use .pw.ts for playwright (vs. bun) tests */
|
|
25
|
-
testMatch: /.*\.pw\.ts/,
|
|
26
|
-
use: {
|
|
27
|
-
/* Base URL to use in actions like `await page.goto('/')`. */
|
|
28
|
-
// baseURL: 'http://127.0.0.1:3000',
|
|
29
|
-
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
|
30
|
-
trace: 'on-first-retry',
|
|
31
|
-
/* allow https on localhost with self-signed certificate */
|
|
32
|
-
ignoreHTTPSErrors: true,
|
|
33
|
-
},
|
|
34
|
-
/* Configure projects for major browsers */
|
|
35
|
-
projects: [
|
|
36
|
-
{
|
|
37
|
-
name: 'chromium',
|
|
38
|
-
use: { ...devices['Desktop Chrome'] },
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
name: 'firefox',
|
|
42
|
-
use: { ...devices['Desktop Firefox'] },
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
name: 'webkit',
|
|
46
|
-
use: { ...devices['Desktop Safari'] },
|
|
47
|
-
},
|
|
48
|
-
/* Test against mobile viewports. */
|
|
49
|
-
// {
|
|
50
|
-
// name: 'Mobile Chrome',
|
|
51
|
-
// use: { ...devices['Pixel 5'] },
|
|
52
|
-
// },
|
|
53
|
-
// {
|
|
54
|
-
// name: 'Mobile Safari',
|
|
55
|
-
// use: { ...devices['iPhone 12'] },
|
|
56
|
-
// },
|
|
57
|
-
/* Test against branded browsers. */
|
|
58
|
-
// {
|
|
59
|
-
// name: 'Microsoft Edge',
|
|
60
|
-
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
|
61
|
-
// },
|
|
62
|
-
// {
|
|
63
|
-
// name: 'Google Chrome',
|
|
64
|
-
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
|
65
|
-
// },
|
|
66
|
-
],
|
|
67
|
-
/* Run your local dev server before starting the tests */
|
|
68
|
-
// webServer: {
|
|
69
|
-
// command: 'npm run start',
|
|
70
|
-
// url: 'http://127.0.0.1:3000',
|
|
71
|
-
// reuseExistingServer: !process.env.CI,
|
|
72
|
-
// },
|
|
73
|
-
});
|