@kubex/zinc 1.0.108 → 1.0.110

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.
@@ -333,6 +333,8 @@ Explicit library specification:
333
333
  <zn-icon src="test1@example.com" library="gravatar" size="48"></zn-icon>
334
334
  ```
335
335
 
336
+ Email addresses are trimmed, lowercased, and hashed with SHA-256 before the Gravatar request is made.
337
+
336
338
  Using shorthand notation:
337
339
 
338
340
  ```html:preview
@@ -348,6 +350,8 @@ Libravatar is an open-source alternative to Gravatar. Use it for federated avata
348
350
  <zn-icon src="user@example.com" library="libravatar" size="48" round></zn-icon>
349
351
  ```
350
352
 
353
+ Email addresses are trimmed, lowercased, and hashed with SHA-256 before the Libravatar request is made.
354
+
351
355
  Similar to Gravatar, you can specify fallback options:
352
356
 
353
357
  ```html:preview
@@ -727,4 +731,4 @@ your HTML like this:
727
731
  grid-template-columns: repeat(4, 1fr);
728
732
  }
729
733
  }
730
- </style>
734
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.0.108",
3
+ "version": "1.0.110",
4
4
  "description": "A collection of web components for building web applications based off of @shoelace-style/Shoelace",
5
5
  "keywords": [
6
6
  "web components",
@@ -139,14 +139,13 @@
139
139
 
140
140
  &:hover:not([disabled]), &:active:not([disabled]) {
141
141
  background-color: rgb(var(--btn-color));
142
- color: white;
143
142
  }
144
143
 
145
144
  &.button--secondary {
146
- border-color: #F3EFFF;
145
+ border-color: var(--zn-color-neutral-100);
147
146
 
148
147
  &:hover:not([disabled]), &:active:not([disabled]) {
149
- background-color: rgb(221, 218, 236);
148
+ background-color: var(--zn-color-neutral-100);
150
149
  }
151
150
  }
152
151
  }
@@ -321,6 +321,12 @@ export default class ZnDatepicker extends ZincElement implements ZincFormControl
321
321
  return;
322
322
  }
323
323
 
324
+ if (event.key === 'Escape') {
325
+ this.input.blur();
326
+ event.stopPropagation();
327
+ return;
328
+ }
329
+
324
330
  // Allow navigation and control keys
325
331
  const allowedKeys = [
326
332
  'Backspace', 'Delete', 'Tab', 'Escape', 'Enter',
@@ -1,5 +1,6 @@
1
1
  import '../../../dist/zn.min.js';
2
- import { expect, fixture, html } from '@open-wc/testing';
2
+ import {expect, fixture, html} from '@open-wc/testing';
3
+ import type ZnDatepicker from './datepicker.component';
3
4
 
4
5
  describe('<zn-datepicker>', () => {
5
6
  it('should render a component', async () => {
@@ -7,4 +8,34 @@ describe('<zn-datepicker>', () => {
7
8
 
8
9
  expect(el).to.exist;
9
10
  });
11
+
12
+ describe('Escape key', () => {
13
+ it('blurs the inner input when focused and the calendar is closed', async () => {
14
+ const el = await fixture<ZnDatepicker>(html`<zn-datepicker></zn-datepicker>`);
15
+ await el.updateComplete;
16
+
17
+ // Focus shows AirDatepicker's calendar (showEvent: 'focus'). Per spec, Escape
18
+ // while calendar is open is handled by AirDatepicker; our handler engages once
19
+ // the calendar is closed. Hide it explicitly to simulate the calendar-closed
20
+ // state, then press Escape.
21
+ el.focus();
22
+ await el.updateComplete;
23
+ const dp = (el as unknown as { _instance?: { hide: () => void; visible: boolean } })._instance;
24
+ dp?.hide();
25
+ await el.updateComplete;
26
+ el.input.focus();
27
+ await el.updateComplete;
28
+ expect(el.shadowRoot!.activeElement).to.equal(el.input);
29
+
30
+ el.input.dispatchEvent(new KeyboardEvent('keydown', {
31
+ key: 'Escape',
32
+ bubbles: true,
33
+ composed: true,
34
+ cancelable: true
35
+ }));
36
+ await el.updateComplete;
37
+
38
+ expect(el.shadowRoot!.activeElement, 'inner input should be blurred').to.not.equal(el.input);
39
+ });
40
+ });
10
41
  });
@@ -74,14 +74,14 @@ export default class ZnFormGroup extends ZincElement {
74
74
  </zn-tooltip>`
75
75
  : ''}
76
76
  </label>
77
+ </div>` : html``}
77
78
 
78
- ${hasHelpText ? html`
79
- <div
80
- part="form-control-help-text"
81
- id="help-text"
82
- class="form-control__help-text">
83
- <slot name="help-text">${this.helpText}</slot>
84
- </div>` : html``}
79
+ ${hasHelpText ? html`
80
+ <div
81
+ part="form-control-help-text"
82
+ id="help-text"
83
+ class="form-control__help-text">
84
+ <slot name="help-text">${this.helpText}</slot>
85
85
  </div>` : html``}
86
86
 
87
87
  <div part="form-control-input" class="form-control-input">
@@ -1,8 +1,8 @@
1
1
  import {choose} from 'lit/directives/choose.js';
2
2
  import {classMap} from "lit/directives/class-map.js";
3
3
  import {type CSSResultGroup, html, render, unsafeCSS} from 'lit';
4
- import {md5} from '../../utilities/md5';
5
4
  import {property} from 'lit/decorators.js';
5
+ import {sha256} from '../../utilities/sha256';
6
6
  import {styleMap} from "lit/directives/style-map.js";
7
7
  import ZincElement from '../../internal/zinc-element';
8
8
 
@@ -30,7 +30,7 @@ export type IconColor =
30
30
  | "violet"
31
31
  | "pink"
32
32
  | "grey"
33
- | (string & {});
33
+ | (string & Record<never, never>);
34
34
 
35
35
  /**
36
36
  * @summary Short summary of the component's intended use.
@@ -127,40 +127,37 @@ export default class ZnIcon extends ZincElement {
127
127
  connectedCallback() {
128
128
  super.connectedCallback();
129
129
 
130
+ let hashFragment = '';
130
131
 
131
132
  if (this.src && this.src.includes('#')) {
132
- const split = this.src.split('#');
133
+ const split = this.src.split('#', 2);
133
134
  this.src = split[0];
134
- const attributes = split[1].split(',');
135
- attributes.forEach(attr => {
136
- if (attr === "round") {
137
- this.round = true;
138
- }
139
- if (attr === "tile") {
140
- this.tile = true;
141
- }
142
- if (attr === "depth") {
143
- this.depth = true;
144
- }
145
- });
135
+ hashFragment = split[1] ?? '';
146
136
  }
147
137
 
148
138
  if (this.src && this.src.includes('@')) {
149
- const split = this.src.split('@');
150
- if (split[1].includes('.')) {
151
- this.library = "gravatar";
152
- } else if ((this.library === undefined) && split[1] !== "") {
139
+ const atIndex = this.src.lastIndexOf('@');
140
+ const libraryOrDomain = this.src.slice(atIndex + 1);
141
+
142
+ if (libraryOrDomain.includes('.')) {
143
+ this.library = this.library ?? "gravatar";
144
+ } else if ((this.library === undefined) && libraryOrDomain !== "") {
153
145
  // if split[1] is a valid library name, set it
154
- this.library = this.convertToLibrary(split[1]);
155
- this.src = split[0];
146
+ this.library = this.convertToLibrary(libraryOrDomain);
147
+ this.src = this.src.slice(0, atIndex);
156
148
  }
157
149
 
158
150
  if (this.library === "gravatar" || this.library === "libravatar") {
159
- this.ravatarOptions();
160
- this.src = md5(this.src);
151
+ this.applyHashFragment(hashFragment);
152
+ this.src = sha256(this.normalizeRavatarEmail(this.src));
153
+ } else {
154
+ this.applyHashFragment(hashFragment);
161
155
  }
162
156
  } else if (!this.library && this.src && !this.src.includes('/')) {
157
+ this.applyHashFragment(hashFragment);
163
158
  this.library = this.defaultLibrary;
159
+ } else {
160
+ this.applyHashFragment(hashFragment);
164
161
  }
165
162
 
166
163
  // load the material icons font if the library is set to material
@@ -174,18 +171,42 @@ export default class ZnIcon extends ZincElement {
174
171
  <link
175
172
  href="https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined|Material+Icons|Material+Icons+Round|Material+Icons+Sharp|Material+Icons+Two+Tone|Material+Icons+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
176
173
  rel="stylesheet">`, document.head);
177
-
178
- this.ravatarOptions();
179
174
  }
180
175
 
181
- ravatarOptions() {
182
- if ((this.library === "gravatar" || this.library === "libravatar") && this.src.includes('#')) {
183
- const split = this.src.split('#');
184
- this.gravatarOptions = "&d=" + split[1];
185
- this.src = split[0];
176
+ private applyHashFragment(hashFragment: string) {
177
+ if (!hashFragment) {
178
+ return;
179
+ }
180
+
181
+ const attributes = hashFragment.split(',');
182
+
183
+ attributes.forEach(attr => {
184
+ if (attr === 'round') {
185
+ this.round = true;
186
+ }
187
+
188
+ if (attr === 'tile') {
189
+ this.tile = true;
190
+ }
191
+
192
+ if (attr === 'depth') {
193
+ this.depth = true;
194
+ }
195
+ });
196
+
197
+ if ((this.library === 'gravatar' || this.library === 'libravatar')) {
198
+ const defaultOption = attributes.find(attr => !['round', 'tile', 'depth'].includes(attr));
199
+
200
+ if (defaultOption) {
201
+ this.gravatarOptions = `&d=${defaultOption}`;
202
+ }
186
203
  }
187
204
  }
188
205
 
206
+ private normalizeRavatarEmail(email: string) {
207
+ return email.trim().toLowerCase();
208
+ }
209
+
189
210
  render() {
190
211
  return html`
191
212
  <div class="icon-wrapper">
@@ -1,5 +1,6 @@
1
1
  import '../../../dist/zn.min.js';
2
2
  import { expect, fixture, html } from '@open-wc/testing';
3
+ import type ZnIcon from './icon.component';
3
4
 
4
5
  describe('<zn-icon>', () => {
5
6
  it('should render a component', async () => {
@@ -7,4 +8,26 @@ describe('<zn-icon>', () => {
7
8
 
8
9
  expect(el).to.exist;
9
10
  });
11
+
12
+ it('should build Gravatar URLs with a normalized SHA-256 hash', async () => {
13
+ const el = await fixture<ZnIcon>(html`
14
+ <zn-icon src="MyEmailAddress@example.com " library="gravatar" size="48"></zn-icon>
15
+ `);
16
+
17
+ const image = el.shadowRoot?.querySelector('img');
18
+ expect(image?.getAttribute('src')).to.equal(
19
+ 'https://www.gravatar.com/avatar/84059b07d4be67b806386c0aad8070a23f18836bbaae342275dc0a83414c32ee?s=48'
20
+ );
21
+ });
22
+
23
+ it('should pass fallback options through to Libravatar', async () => {
24
+ const el = await fixture<ZnIcon>(html`
25
+ <zn-icon src="nonexistent@example.com#identicon" library="libravatar" size="48"></zn-icon>
26
+ `);
27
+
28
+ const image = el.shadowRoot?.querySelector('img');
29
+ expect(image?.getAttribute('src')).to.equal(
30
+ 'https://seccdn.libravatar.org/avatar/04e8703fcfb60849fb5c596abc8f3d7547c7e0099c3806a3ffec77a95dc02e25?s=48&d=identicon'
31
+ );
32
+ });
10
33
  });
@@ -547,6 +547,12 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
547
547
  }
548
548
 
549
549
  private handleKeyDown(event: KeyboardEvent) {
550
+ if (event.key === 'Escape') {
551
+ this.input.blur();
552
+ event.stopPropagation();
553
+ return;
554
+ }
555
+
550
556
  const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
551
557
 
552
558
  // Pressing enter when focused on the input should submit the form like a native input, be we wait a tick before
@@ -1,5 +1,6 @@
1
1
  import '../../../dist/zn.min.js';
2
- import { expect, fixture, html } from '@open-wc/testing';
2
+ import {expect, fixture, html} from '@open-wc/testing';
3
+ import type ZnInput from './input.component';
3
4
 
4
5
  describe('<zn-input>', () => {
5
6
  it('should render a component', async () => {
@@ -7,4 +8,36 @@ describe('<zn-input>', () => {
7
8
 
8
9
  expect(el).to.exist;
9
10
  });
11
+
12
+ describe('Escape key', () => {
13
+ it('blurs the inner input and stops propagation when focused', async () => {
14
+ const wrapper = await fixture<HTMLDivElement>(html`
15
+ <div>
16
+ <zn-input></zn-input>
17
+ </div>
18
+ `);
19
+ const el = wrapper.querySelector('zn-input') as ZnInput;
20
+ await el.updateComplete;
21
+
22
+ let wrapperEscapes = 0;
23
+ wrapper.addEventListener('keydown', (e) => {
24
+ if ((e as KeyboardEvent).key === 'Escape') wrapperEscapes++;
25
+ });
26
+
27
+ el.focus();
28
+ await el.updateComplete;
29
+ expect(el.shadowRoot!.activeElement).to.equal(el.input);
30
+
31
+ el.input.dispatchEvent(new KeyboardEvent('keydown', {
32
+ key: 'Escape',
33
+ bubbles: true,
34
+ composed: true,
35
+ cancelable: true
36
+ }));
37
+ await el.updateComplete;
38
+
39
+ expect(el.shadowRoot!.activeElement, 'inner input should be blurred').to.not.equal(el.input);
40
+ expect(wrapperEscapes, 'wrapper should not see Escape on press 1').to.equal(0);
41
+ });
42
+ });
10
43
  });
@@ -484,6 +484,13 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
484
484
  this.displayInput.focus({preventScroll: true});
485
485
  }
486
486
 
487
+ // Blur the display input when Escape is pressed and the dropdown is closed,
488
+ // so a second Escape press can bubble (e.g. close a containing dialog).
489
+ if (event.key === 'Escape' && !this.open) {
490
+ event.stopPropagation();
491
+ this.displayInput.blur();
492
+ }
493
+
487
494
  // Handle enter and space. When pressing space, we allow for type to select behaviors so if there's anything in the
488
495
  // buffer we _don't_ close it. When search is enabled, space should type into the input, not toggle.
489
496
  if (event.key === 'Enter' || (event.key === ' ' && !this.search && this.typeToSelectString === '')) {
@@ -152,4 +152,28 @@ describe('<zn-select>', () => {
152
152
  expect(visibleOptions[0].value).to.equal('apple');
153
153
  });
154
154
  });
155
+
156
+ describe('Escape key', () => {
157
+ it('blurs the display input when dropdown is closed and Escape is pressed', async () => {
158
+ const el = await fixture<ZnSelect>(html`<zn-select></zn-select>`);
159
+ await el.updateComplete;
160
+
161
+ const displayInput = el.shadowRoot!.querySelector<HTMLInputElement>('.select__display-input')!;
162
+ el.focus();
163
+ await el.updateComplete;
164
+ expect(el.shadowRoot!.activeElement).to.equal(displayInput);
165
+ expect(el.open).to.be.false;
166
+
167
+ displayInput.dispatchEvent(new KeyboardEvent('keydown', {
168
+ key: 'Escape',
169
+ bubbles: true,
170
+ composed: true,
171
+ cancelable: true
172
+ }));
173
+ await el.updateComplete;
174
+
175
+ expect(el.shadowRoot!.activeElement, 'display input should be blurred').to.not.equal(displayInput);
176
+ });
177
+
178
+ });
155
179
  });
@@ -233,6 +233,13 @@ export default class ZnTextarea extends ZincElement implements ZincFormControl {
233
233
  this.formControlController.emitInvalidEvent(event);
234
234
  }
235
235
 
236
+ private handleKeyDown(event: KeyboardEvent) {
237
+ if (event.key === 'Escape') {
238
+ this.input.blur();
239
+ event.stopPropagation();
240
+ }
241
+ }
242
+
236
243
  private setTextareaHeight() {
237
244
  if (this.resize === 'auto') {
238
245
  this.input.style.height = 'auto';
@@ -436,6 +443,7 @@ export default class ZnTextarea extends ZincElement implements ZincFormControl {
436
443
  @change=${this.handleChange}
437
444
  @input=${this.handleInput}
438
445
  @invalid=${this.handleInvalid}
446
+ @keydown=${this.handleKeyDown}
439
447
  @focus=${this.handleFocus}
440
448
  @blur=${this.handleBlur}
441
449
  ></textarea>
@@ -1,10 +1,43 @@
1
1
  import '../../../dist/zn.min.js';
2
- import { expect, fixture, html } from '@open-wc/testing';
2
+ import {expect, fixture, html} from '@open-wc/testing';
3
+ import type ZnTextarea from './textarea.component';
3
4
 
4
- describe('<zn-text-area>', () => {
5
+ describe('<zn-textarea>', () => {
5
6
  it('should render a component', async () => {
6
- const el = await fixture(html` <zn-text-area></zn-text-area> `);
7
+ const el = await fixture(html` <zn-textarea></zn-textarea> `);
7
8
 
8
9
  expect(el).to.exist;
9
10
  });
11
+
12
+ describe('Escape key', () => {
13
+ it('blurs the inner textarea and stops propagation when focused', async () => {
14
+ const wrapper = await fixture<HTMLDivElement>(html`
15
+ <div>
16
+ <zn-textarea></zn-textarea>
17
+ </div>
18
+ `);
19
+ const el = wrapper.querySelector('zn-textarea') as ZnTextarea;
20
+ await el.updateComplete;
21
+
22
+ let wrapperEscapes = 0;
23
+ wrapper.addEventListener('keydown', (e) => {
24
+ if ((e as KeyboardEvent).key === 'Escape') wrapperEscapes++;
25
+ });
26
+
27
+ el.focus();
28
+ await el.updateComplete;
29
+ expect(el.shadowRoot!.activeElement).to.equal(el.input);
30
+
31
+ el.input.dispatchEvent(new KeyboardEvent('keydown', {
32
+ key: 'Escape',
33
+ bubbles: true,
34
+ composed: true,
35
+ cancelable: true
36
+ }));
37
+ await el.updateComplete;
38
+
39
+ expect(el.shadowRoot!.activeElement, 'inner textarea should be blurred').to.not.equal(el.input);
40
+ expect(wrapperEscapes, 'wrapper should not see Escape on press 1').to.equal(0);
41
+ });
42
+ });
10
43
  });
@@ -1,24 +1,28 @@
1
1
  @use "../../wc";
2
2
 
3
+ :host {
4
+ display: flex;
5
+ min-width: 0;
6
+ max-width: 240px;
7
+ }
3
8
 
4
9
  .tile__property {
5
10
  display: flex;
6
11
  align-items: center;
7
12
  flex-direction: row;
8
- width: 150px;
9
- flex-basis: 150px;
10
- max-width: 200px;
13
+ min-width: 0;
14
+ width: 100%;
11
15
 
12
16
  .tile__property_container {
13
17
  display: flex;
14
18
  flex-direction: column;
15
19
  overflow: hidden;
16
- white-space: nowrap;
17
- text-overflow: ellipsis;
20
+ min-width: 0;
18
21
  }
19
22
 
20
23
  .tile__icon {
21
24
  margin-right: 15px;
25
+ flex-shrink: 0;
22
26
  }
23
27
 
24
28
  .tile__caption {
@@ -29,6 +33,7 @@
29
33
  color: rgb(var(--zn-text-heading));
30
34
  text-overflow: ellipsis;
31
35
  overflow: hidden;
36
+ white-space: nowrap;
32
37
  }
33
38
 
34
39
  .tile__description {
@@ -39,5 +44,6 @@
39
44
  color: rgb(var(--zn-text-color));
40
45
  text-overflow: ellipsis;
41
46
  overflow: hidden;
47
+ white-space: nowrap;
42
48
  }
43
49
  }
@@ -0,0 +1,114 @@
1
+ /* eslint-disable */
2
+ export function sha256(input: string): string {
3
+ function rightRotate(value: number, amount: number) {
4
+ return (value >>> amount) | (value << (32 - amount));
5
+ }
6
+
7
+ function utf8Encode(string: string) {
8
+ string = string.replace(/\r\n/g, '\n');
9
+ let utfText = '';
10
+
11
+ for (let index = 0; index < string.length; index++) {
12
+ const charCode = string.charCodeAt(index);
13
+
14
+ if (charCode < 128) {
15
+ utfText += String.fromCharCode(charCode);
16
+ } else if (charCode < 2048) {
17
+ utfText += String.fromCharCode((charCode >> 6) | 192);
18
+ utfText += String.fromCharCode((charCode & 63) | 128);
19
+ } else {
20
+ utfText += String.fromCharCode((charCode >> 12) | 224);
21
+ utfText += String.fromCharCode(((charCode >> 6) & 63) | 128);
22
+ utfText += String.fromCharCode((charCode & 63) | 128);
23
+ }
24
+ }
25
+
26
+ return utfText;
27
+ }
28
+
29
+ const mathPow = Math.pow;
30
+ const maxWord = mathPow(2, 32);
31
+ const words: number[] = [];
32
+ const hash: number[] = [];
33
+ const roundConstants: number[] = [];
34
+ const isComposite: Record<number, boolean> = {};
35
+ let primeCounter = 0;
36
+ let message = utf8Encode(input);
37
+ const messageBitLength = message.length * 8;
38
+
39
+ for (let candidate = 2; primeCounter < 64; candidate++) {
40
+ if (!isComposite[candidate]) {
41
+ for (let index = 0; index < 313; index += candidate) {
42
+ isComposite[index] = true;
43
+ }
44
+
45
+ if (primeCounter < 8) {
46
+ hash[primeCounter] = (mathPow(candidate, 0.5) * maxWord) | 0;
47
+ }
48
+
49
+ roundConstants[primeCounter] = (mathPow(candidate, 1 / 3) * maxWord) | 0;
50
+ primeCounter++;
51
+ }
52
+ }
53
+
54
+ message += '\x80';
55
+
56
+ while (message.length % 64 !== 56) {
57
+ message += '\x00';
58
+ }
59
+
60
+ for (let index = 0; index < message.length; index++) {
61
+ words[index >> 2] |= message.charCodeAt(index) << ((3 - (index % 4)) * 8);
62
+ }
63
+
64
+ words[words.length] = (messageBitLength / maxWord) | 0;
65
+ words[words.length] = messageBitLength;
66
+
67
+ for (let chunkStart = 0; chunkStart < words.length; chunkStart += 16) {
68
+ const workingHash = hash.slice(0, 8);
69
+ const schedule = words.slice(chunkStart, chunkStart + 16);
70
+
71
+ for (let round = 16; round < 64; round++) {
72
+ const word15 = schedule[round - 15];
73
+ const word2 = schedule[round - 2];
74
+ const sigma0 = rightRotate(word15, 7) ^ rightRotate(word15, 18) ^ (word15 >>> 3);
75
+ const sigma1 = rightRotate(word2, 17) ^ rightRotate(word2, 19) ^ (word2 >>> 10);
76
+
77
+ schedule[round] = (schedule[round - 16] + sigma0 + schedule[round - 7] + sigma1) | 0;
78
+ }
79
+
80
+ for (let round = 0; round < 64; round++) {
81
+ const a = workingHash[0];
82
+ const b = workingHash[1];
83
+ const c = workingHash[2];
84
+ const d = workingHash[3];
85
+ const e = workingHash[4];
86
+ const f = workingHash[5];
87
+ const g = workingHash[6];
88
+ const h = workingHash[7];
89
+ const sigma1 = rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25);
90
+ const choose = (e & f) ^ (~e & g);
91
+ const temp1 = (h + sigma1 + choose + roundConstants[round] + schedule[round]) | 0;
92
+ const sigma0 = rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22);
93
+ const majority = (a & b) ^ (a & c) ^ (b & c);
94
+ const temp2 = (sigma0 + majority) | 0;
95
+
96
+ workingHash[7] = g;
97
+ workingHash[6] = f;
98
+ workingHash[5] = e;
99
+ workingHash[4] = (d + temp1) | 0;
100
+ workingHash[3] = c;
101
+ workingHash[2] = b;
102
+ workingHash[1] = a;
103
+ workingHash[0] = (temp1 + temp2) | 0;
104
+ }
105
+
106
+ for (let index = 0; index < 8; index++) {
107
+ hash[index] = (hash[index] + workingHash[index]) | 0;
108
+ }
109
+ }
110
+
111
+ return hash
112
+ .map(value => (value >>> 0).toString(16).padStart(8, '0'))
113
+ .join('');
114
+ }