keyframekit 1.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +122 -0
  3. package/dist/KeyframeKit.d.ts +139 -0
  4. package/dist/KeyframeKit.d.ts.map +1 -0
  5. package/dist/KeyframeKit.js +294 -0
  6. package/dist/KeyframeKit.js.map +1 -0
  7. package/docs/.vitepress/components/Playground/Playground.js +243 -0
  8. package/docs/.vitepress/components/Playground/Playground.vue +206 -0
  9. package/docs/.vitepress/components/Playground/defaultExample.js +175 -0
  10. package/docs/.vitepress/components/Playground/interFont.js +14 -0
  11. package/docs/.vitepress/components/Playground/themes/githubDark.js +402 -0
  12. package/docs/.vitepress/components/Playground/themes/githubLight.js +399 -0
  13. package/docs/.vitepress/components/Playground/themes.js +24 -0
  14. package/docs/.vitepress/config.ts +64 -0
  15. package/docs/.vitepress/referenceNavigation.ts +37 -0
  16. package/docs/.vitepress/theme/base-styles.css +91 -0
  17. package/docs/.vitepress/theme/env.d.ts +5 -0
  18. package/docs/.vitepress/theme/index.ts +40 -0
  19. package/docs/docs/index.md +136 -0
  20. package/docs/docs/public/icon.png +0 -0
  21. package/docs/docs/public/playground/KeyframeKit/dist/KeyframeKit.d.ts +139 -0
  22. package/docs/docs/public/playground/KeyframeKit/dist/KeyframeKit.d.ts.map +1 -0
  23. package/docs/docs/public/playground/KeyframeKit/dist/KeyframeKit.js +294 -0
  24. package/docs/docs/public/playground/KeyframeKit/dist/KeyframeKit.js.map +1 -0
  25. package/docs/docs/reference/_media/LICENSE +21 -0
  26. package/docs/docs/reference/classes/KeyframeEffectParameters.md +95 -0
  27. package/docs/docs/reference/classes/ParsedKeyframes.md +49 -0
  28. package/docs/docs/reference/index.md +20 -0
  29. package/docs/docs/reference/interfaces/KeyframesFactory.md +151 -0
  30. package/docs/docs/reference/navigation.json +64 -0
  31. package/docs/docs/reference/type-aliases/KeyframeArgument.md +9 -0
  32. package/docs/docs/reference/type-aliases/KeyframesFactorySource.md +9 -0
  33. package/docs/docs/reference/type-aliases/ParsedKeyframesRules.md +15 -0
  34. package/docs/docs/reference/variables/default.md +7 -0
  35. package/docs/package.json +25 -0
  36. package/docs/typedoc/plugin-param-names.js +52 -0
  37. package/docs/typedoc.json +63 -0
  38. package/package.json +37 -0
  39. package/src/KeyframeKit.ts +508 -0
  40. package/tsconfig.json +47 -0
  41. package/vercel.json +13 -0
@@ -0,0 +1,508 @@
1
+ //
2
+ // KeyframeKit
3
+ // Ben Hatsor
4
+ //
5
+ // See README.md for usage.
6
+ //
7
+
8
+
9
+ const CHARS = {
10
+ PERCENT_SIGN: '%',
11
+ HYPHEN_MINUS: '-',
12
+ DOUBLE_HYPHEN_MINUS: '--',
13
+ WEBKIT_PREFIX: '-webkit-'
14
+ } as const;
15
+
16
+
17
+ export type KeyframesFactorySource = StyleSheetList | CSSStyleSheet;
18
+
19
+ class KeyframesFactory {
20
+
21
+ readonly Error = {
22
+ KeyframesRuleNameTypeError: class KeyframesRuleNameTypeError extends TypeError {
23
+ message = `Keyframes rule name must be a string.`;
24
+ },
25
+ SourceTypeError: class SourceTypeError extends TypeError {
26
+ message = `Source must be either a Document, a ShadowRoot or a CSSStyleSheet instance.`;
27
+ },
28
+ StyleSheetImportError: class StyleSheetImportError extends Error {
29
+ message = `The stylesheet could not be imported.`;
30
+ }
31
+ } as const;
32
+
33
+
34
+ async getDocumentStyleSheetsOnLoad({ document = window.document }: {
35
+ document?: Document
36
+ } = {}) {
37
+
38
+ await waitForDocumentLoad({
39
+ document: document
40
+ });
41
+
42
+ return document.styleSheets;
43
+
44
+ }
45
+
46
+
47
+ /** @remarks
48
+ * Note: `@import` rules won't be resolved in imported stylesheets.
49
+ * See https://github.com/WICG/construct-stylesheets/issues/119#issuecomment-588352418. */
50
+ async importStyleSheet(url: string) {
51
+
52
+ const resp = await fetch(url);
53
+
54
+ if (!resp.ok) {
55
+ throw new this.Error.StyleSheetImportError();
56
+ }
57
+
58
+ const respText = await resp.text();
59
+
60
+ // remove file name from URL to get base URL
61
+ const baseURL = url.split('/').slice(0, -1).join('/');
62
+
63
+ const styleSheet = new CSSStyleSheet({
64
+ baseURL: baseURL
65
+ });
66
+
67
+ await styleSheet.replace(respText);
68
+
69
+ return styleSheet;
70
+
71
+ }
72
+
73
+
74
+ /** @param obj.of The name of the `@keyframes` rule to get keyframes from. */
75
+ getStyleSheetKeyframes({ of: ruleName, in: source }: {
76
+ of: string,
77
+ in: KeyframesFactorySource
78
+ }): ParsedKeyframes | undefined {
79
+
80
+ if (typeof ruleName !== 'string') {
81
+ throw new this.Error.KeyframesRuleNameTypeError();
82
+ }
83
+
84
+ if (source instanceof StyleSheetList) {
85
+
86
+ return this.#getStyleSheetKeyframesInStyleSheetList({
87
+ of: ruleName,
88
+ styleSheetList: source
89
+ });
90
+
91
+ } else if (source instanceof CSSStyleSheet) {
92
+
93
+ return this.#getStyleSheetKeyframesInStyleSheet({
94
+ of: ruleName,
95
+ styleSheet: source
96
+ });
97
+
98
+ } else {
99
+
100
+ throw new this.Error.SourceTypeError();
101
+
102
+ }
103
+
104
+ }
105
+
106
+ #getStyleSheetKeyframesInStyleSheetList({ of: ruleName, styleSheetList }: {
107
+ of: string,
108
+ styleSheetList: StyleSheetList
109
+ }): ParsedKeyframes | undefined {
110
+
111
+ for (const styleSheet of styleSheetList) {
112
+
113
+ const keyframesRule = this.#getStyleSheetKeyframesInStyleSheet({
114
+ of: ruleName,
115
+ styleSheet: styleSheet
116
+ });
117
+
118
+ if (keyframesRule !== undefined) {
119
+ return keyframesRule;
120
+ }
121
+
122
+ }
123
+
124
+ }
125
+
126
+ #getStyleSheetKeyframesInStyleSheet({ of: ruleName, styleSheet }: {
127
+ of: string,
128
+ styleSheet: CSSStyleSheet
129
+ }): ParsedKeyframes | undefined {
130
+
131
+ for (const rule of styleSheet.cssRules) {
132
+
133
+ if (!(rule instanceof CSSKeyframesRule)) {
134
+ continue;
135
+ }
136
+
137
+ if (rule.name === ruleName) {
138
+
139
+ const keyframes = this.parseKeyframesRule({
140
+ rule: rule
141
+ });
142
+
143
+ return keyframes;
144
+
145
+ }
146
+
147
+ }
148
+
149
+ }
150
+
151
+
152
+ getAllStyleSheetKeyframesRules({ in: source }: {
153
+ in: KeyframesFactorySource
154
+ }): ParsedKeyframesRules {
155
+
156
+ if (source instanceof StyleSheetList) {
157
+
158
+ return this.#getAllStyleSheetKeyframesRulesInStyleSheetList({
159
+ styleSheetList: source
160
+ });
161
+
162
+ } else if (source instanceof CSSStyleSheet) {
163
+
164
+ return this.#getAllStyleSheetKeyframesRulesInStyleSheet({
165
+ styleSheet: source
166
+ });
167
+
168
+ } else {
169
+
170
+ throw new this.Error.SourceTypeError();
171
+
172
+ }
173
+
174
+ }
175
+
176
+ #getAllStyleSheetKeyframesRulesInStyleSheetList({ styleSheetList }: {
177
+ styleSheetList: StyleSheetList
178
+ }): ParsedKeyframesRules {
179
+
180
+ let keyframesRules: ParsedKeyframesRules = {};
181
+
182
+ for (const styleSheet of styleSheetList) {
183
+
184
+ const styleSheetKeyframesRules = this.#getAllStyleSheetKeyframesRulesInStyleSheet({
185
+ styleSheet: styleSheet
186
+ });
187
+
188
+ keyframesRules = {
189
+ ...keyframesRules,
190
+ ...styleSheetKeyframesRules
191
+ };
192
+
193
+ }
194
+
195
+ return keyframesRules;
196
+
197
+ }
198
+
199
+ #getAllStyleSheetKeyframesRulesInStyleSheet({ styleSheet }: {
200
+ styleSheet: CSSStyleSheet
201
+ }): ParsedKeyframesRules {
202
+
203
+ let keyframesRules: ParsedKeyframesRules = {};
204
+
205
+ for (const rule of styleSheet.cssRules) {
206
+
207
+ if (!(rule instanceof CSSKeyframesRule)) {
208
+ continue;
209
+ }
210
+
211
+ const keyframes = this.parseKeyframesRule({
212
+ rule: rule
213
+ });
214
+
215
+ keyframesRules[rule.name] = keyframes;
216
+
217
+ }
218
+
219
+ return keyframesRules;
220
+
221
+ }
222
+
223
+
224
+ parseKeyframesRule({ rule: keyframes }: {
225
+ rule: CSSKeyframesRule
226
+ }): ParsedKeyframes {
227
+
228
+ let parsedKeyframes: Keyframe[] = [];
229
+
230
+ for (const keyframe of keyframes) {
231
+
232
+ // remove trailing '%'
233
+ /// https://drafts.csswg.org/css-animations/#dom-csskeyframerule-keytext
234
+ const percentString = removeSuffix({
235
+ of: keyframe.keyText,
236
+ suffix: CHARS.PERCENT_SIGN
237
+ });
238
+
239
+ const percent = Number(percentString);
240
+
241
+ const offset = percent / 100;
242
+
243
+
244
+ let parsedProperties: KeyframeProperties = {};
245
+
246
+ for (const propertyName of keyframe.style) {
247
+
248
+ /// https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration/getPropertyValue
249
+ const propertyValue = keyframe.style.getPropertyValue(propertyName);
250
+
251
+ /// https://drafts.csswg.org/web-animations-1/#ref-for-animation-property-name-to-idl-attribute-name%E2%91%A0
252
+ const attributeName = this.#animationPropertyNameToIDLAttributeName(
253
+ propertyName
254
+ );
255
+
256
+ parsedProperties[attributeName] = propertyValue;
257
+
258
+ }
259
+
260
+
261
+ const parsedKeyframe: Keyframe = {
262
+ ...parsedProperties,
263
+ offset: offset
264
+ };
265
+
266
+ parsedKeyframes.push(parsedKeyframe);
267
+
268
+ }
269
+
270
+ const parsedKeyframesInstance = new ParsedKeyframes(parsedKeyframes);
271
+
272
+ return parsedKeyframesInstance;
273
+
274
+ }
275
+
276
+
277
+ /** https://drafts.csswg.org/web-animations-1/#animation-property-name-to-idl-attribute-name */
278
+ #animationPropertyNameToIDLAttributeName(property: string) {
279
+
280
+ if (this.#isCustomPropertyName(property)) return property;
281
+
282
+ if (property === 'float') return 'cssFloat';
283
+
284
+ if (property === 'offset') return 'cssOffset';
285
+
286
+ // https://drafts.csswg.org/cssom/#ref-for-supported-css-property%E2%91%A2
287
+ const lowercaseFirst = this.#isWebkitCasedAttribute(property);
288
+
289
+ return this.#cssPropertyToIDLAttribute(property, lowercaseFirst);
290
+
291
+ }
292
+
293
+ /** https://drafts.csswg.org/cssom/#css-property-to-idl-attribute */
294
+ #cssPropertyToIDLAttribute(property: string, lowercaseFirst: boolean = false) {
295
+
296
+ let output = '';
297
+
298
+ let uppercaseNext = false;
299
+
300
+ if (lowercaseFirst) {
301
+ property = property.slice(1);
302
+ }
303
+
304
+ for (const c of property) {
305
+
306
+ if (c === CHARS.HYPHEN_MINUS) {
307
+
308
+ uppercaseNext = true;
309
+
310
+ } else if (uppercaseNext) {
311
+
312
+ uppercaseNext = false;
313
+
314
+ output += c.toUpperCase();
315
+
316
+ } else {
317
+
318
+ output += c;
319
+
320
+ }
321
+
322
+ }
323
+
324
+ return output;
325
+
326
+ }
327
+
328
+ /** https://drafts.csswg.org/css-variables-2/#typedef-custom-property-name */
329
+ #isCustomPropertyName(property: string) {
330
+ return property.startsWith(CHARS.DOUBLE_HYPHEN_MINUS) &&
331
+ property !== CHARS.DOUBLE_HYPHEN_MINUS;
332
+ }
333
+
334
+ /** https://drafts.csswg.org/cssom/#ref-for-supported-css-property%E2%91%A2 */
335
+ #isWebkitCasedAttribute(property: string) {
336
+ return property.startsWith(CHARS.WEBKIT_PREFIX);
337
+ }
338
+
339
+ }
340
+
341
+ export default new KeyframesFactory();
342
+ export type { KeyframesFactory };
343
+
344
+ /** https://drafts.csswg.org/web-animations-1/#processing-a-keyframes-argument */
345
+ type KeyframeProperties = { [propertyName: string]: string };
346
+
347
+
348
+ /** https://drafts.csswg.org/web-animations-1/#processing-a-keyframes-argument */
349
+ export type KeyframeArgument = Keyframe[] | PropertyIndexedKeyframes;
350
+
351
+ /** https://drafts.csswg.org/web-animations-1/#the-keyframeeffect-interface */
352
+ export class KeyframeEffectParameters {
353
+
354
+ keyframes: KeyframeArgument;
355
+ options: KeyframeEffectOptions;
356
+
357
+ /**
358
+ * @param obj.keyframes [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Keyframe_Formats)
359
+ * @param obj.options [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/KeyframeEffect/KeyframeEffect#options)
360
+ */
361
+ constructor({ keyframes, options = {} }: {
362
+ keyframes: KeyframeArgument,
363
+ options?: number | KeyframeEffectOptions
364
+ }) {
365
+ this.keyframes = keyframes;
366
+ this.options = this.#parseOptionsArg(options);
367
+ }
368
+
369
+ /**
370
+ * @param obj.target An element to attach the animation to.
371
+ * @param obj.options Additional keyframe effect options. Can override existing keys.
372
+ * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/KeyframeEffect/KeyframeEffect#options)
373
+ *
374
+ * @see Specifications:
375
+ * - https://drafts.csswg.org/web-animations-1/#the-keyframeeffect-interface
376
+ * - https://drafts.csswg.org/web-animations-1/#the-animation-interface
377
+ */
378
+ toAnimation({ target, options: additionalOptions = {}, timeline = document.timeline }: {
379
+ target: Element | null,
380
+ options?: number | KeyframeEffectOptions,
381
+ timeline?: AnimationTimeline
382
+ }): Animation {
383
+
384
+ additionalOptions = this.#parseOptionsArg(additionalOptions);
385
+
386
+ // override existing option keys with additional options
387
+ const options: KeyframeEffectOptions = {
388
+ ...this.options, ...additionalOptions
389
+ };
390
+
391
+
392
+ const keyframeEffect = new KeyframeEffect(
393
+ target,
394
+ this.keyframes,
395
+ options
396
+ );
397
+
398
+ const animation = new Animation(
399
+ keyframeEffect,
400
+ timeline
401
+ );
402
+
403
+ return animation;
404
+
405
+ }
406
+
407
+ /** https://drafts.csswg.org/web-animations-1/#dom-keyframeeffect-keyframeeffect-target-keyframes-options-options
408
+ https://drafts.csswg.org/web-animations-1/#dom-effecttiming-duration */
409
+ #parseOptionsArg(options: number | KeyframeEffectOptions) {
410
+
411
+ if (typeof options === 'number') {
412
+ return { duration: options };
413
+ }
414
+
415
+ return options;
416
+
417
+ }
418
+
419
+ }
420
+
421
+
422
+ export class ParsedKeyframes {
423
+
424
+ keyframes: Keyframe[];
425
+
426
+ constructor(keyframes: Keyframe[]) {
427
+ this.keyframes = keyframes;
428
+ }
429
+
430
+ /**
431
+ * @param options
432
+ * [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/KeyframeEffect/KeyframeEffect#options)
433
+ */
434
+ toKeyframeEffect(
435
+ options: number | KeyframeEffectOptions | null
436
+ ): KeyframeEffectParameters {
437
+
438
+ let keyframeEffect: KeyframeEffectParameters;
439
+
440
+ // convert (required) nullable to optional
441
+ if (options !== null) {
442
+
443
+ keyframeEffect = new KeyframeEffectParameters({
444
+ keyframes: this.keyframes,
445
+ options: options
446
+ });
447
+
448
+ } else {
449
+
450
+ keyframeEffect = new KeyframeEffectParameters({
451
+ keyframes: this.keyframes
452
+ });
453
+
454
+ }
455
+
456
+ return keyframeEffect;
457
+
458
+ }
459
+
460
+ }
461
+
462
+
463
+ export type ParsedKeyframesRules = {
464
+ [ruleName: string]: ParsedKeyframes
465
+ };
466
+
467
+
468
+
469
+ // MARK: - Util
470
+
471
+ function removeSuffix({ of: string, suffix }: {
472
+ of: string,
473
+ suffix: string
474
+ }) {
475
+
476
+ return string.slice(0, -suffix.length);
477
+
478
+ }
479
+
480
+
481
+ async function waitForDocumentLoad({ document }: {
482
+ document: Document
483
+ }) {
484
+
485
+ if (document.readyState === 'complete') {
486
+ return;
487
+ }
488
+
489
+ const { promise, resolve } = Promise.withResolvers();
490
+
491
+ function onReadyStateChange() {
492
+ if (document.readyState === 'complete') {
493
+ resolve(null);
494
+ }
495
+ }
496
+
497
+ const listener = [
498
+ 'readystatechange',
499
+ onReadyStateChange
500
+ ] as const;
501
+
502
+ document.addEventListener(...listener);
503
+
504
+ await promise;
505
+
506
+ document.removeEventListener(...listener);
507
+
508
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ // Visit https://aka.ms/tsconfig to read more about this file
3
+ "include": [
4
+ "src/**/*"
5
+ ],
6
+ "exclude": [],
7
+ "compilerOptions": {
8
+ // File Layout
9
+ "rootDir": "src",
10
+ "outDir": "dist",
11
+
12
+ // Environment Settings
13
+ // See also https://aka.ms/tsconfig/module
14
+ "module": "esnext",
15
+ "target": "esnext",
16
+ "types": [],
17
+ // For nodejs:
18
+ // "lib": ["esnext"],
19
+ // "types": ["node"],
20
+ // and npm install -D @types/node
21
+
22
+ // Other Outputs
23
+ "sourceMap": true,
24
+ "declaration": true,
25
+ "declarationMap": true,
26
+
27
+ // Stricter Typechecking Options
28
+ "noUncheckedIndexedAccess": true,
29
+ "exactOptionalPropertyTypes": true,
30
+
31
+ // Style Options
32
+ // "noImplicitReturns": true,
33
+ // "noImplicitOverride": true,
34
+ // "noUnusedLocals": true,
35
+ // "noUnusedParameters": true,
36
+ // "noFallthroughCasesInSwitch": true,
37
+ // "noPropertyAccessFromIndexSignature": true,
38
+
39
+ // Recommended Options
40
+ "strict": true,
41
+ "verbatimModuleSyntax": true,
42
+ "isolatedModules": true,
43
+ "noUncheckedSideEffectImports": true,
44
+ "moduleDetection": "force",
45
+ "skipLibCheck": true,
46
+ }
47
+ }
package/vercel.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "headers": [
3
+ {
4
+ "source": "/assets/(.*)",
5
+ "headers": [
6
+ {
7
+ "key": "Cache-Control",
8
+ "value": "max-age=31536000, immutable"
9
+ }
10
+ ]
11
+ }
12
+ ]
13
+ }