@oslokommune/punkt-elements 16.13.2 → 16.15.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 (157) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/dist/calendar-Ch1dgnBo.js +656 -0
  3. package/dist/calendar-DkjXla0I.cjs +90 -0
  4. package/dist/{card-CQYxrtL0.cjs → card-C3POnP3O.cjs} +1 -1
  5. package/dist/{card-Bir3-pl2.js → card-CEDbo1t7.js} +2 -2
  6. package/dist/{checkbox-Cspxcvt_.js → checkbox-Bjp43NxA.js} +1 -1
  7. package/dist/{checkbox-BARDb9iV.cjs → checkbox-nTbihMxW.cjs} +1 -1
  8. package/dist/{combobox-CGbFTWq_.cjs → combobox-C4Q6kQEs.cjs} +1 -1
  9. package/dist/{combobox-BcdLZRDU.js → combobox-CL9BLuxt.js} +6 -6
  10. package/dist/components/fileupload/fileupload-base.d.ts +116 -0
  11. package/dist/components/fileupload/fileupload-operations.d.ts +4 -0
  12. package/dist/components/fileupload/fileupload-renderers.d.ts +28 -0
  13. package/dist/components/fileupload/fileupload-types.d.ts +70 -0
  14. package/dist/components/fileupload/fileupload.d.ts +25 -0
  15. package/dist/components/fileupload/fileupload.test.d.ts +0 -0
  16. package/dist/components/fileupload/index.d.ts +4 -0
  17. package/dist/components/index.d.ts +2 -0
  18. package/dist/components/input-wrapper/input-wrapper.d.ts +2 -0
  19. package/dist/controllers/focus-modality-controller.d.ts +31 -0
  20. package/dist/converters-Bffq_YDm.cjs +1 -0
  21. package/dist/converters-jauYkvNh.js +1378 -0
  22. package/dist/{datepicker-OVd8pmnZ.js → datepicker-BUK1Qanw.js} +22 -21
  23. package/dist/{datepicker-DpmlsnH_.cjs → datepicker-DYd3JCs_.cjs} +10 -10
  24. package/dist/docs/components/fileupload.d.ts +27 -0
  25. package/dist/fileupload-BRqioKSY.cjs +396 -0
  26. package/dist/fileupload-Bnl303X0.js +1372 -0
  27. package/dist/fileupload.d.ts +1 -0
  28. package/dist/{header-DWgPgGfS.cjs → header-Dps8o-ZY.cjs} +1 -1
  29. package/dist/{header-a6ztVNp3.js → header-T5O6sfCN.js} +3 -3
  30. package/dist/{helptext-CCBKKLus.js → helptext-C5rG4VkA.js} +2 -2
  31. package/dist/{helptext-D-3QfZv5.cjs → helptext-DQgStO4D.cjs} +1 -1
  32. package/dist/{input-element-Bw1s6NlF.js → input-element-CjebcQd1.js} +3 -3
  33. package/dist/{input-element-B1yxcfz4.cjs → input-element-FGcr5hGd.cjs} +1 -1
  34. package/dist/input-wrapper-B_XFi211.cjs +55 -0
  35. package/dist/{input-wrapper-YfTmpSSY.js → input-wrapper-DBpMVSxt.js} +41 -15
  36. package/dist/{listbox-DuBgTFvH.js → listbox-0NHJvNjz.js} +2 -2
  37. package/dist/{listbox-DCA2JoPV.cjs → listbox-CcThiNBY.cjs} +1 -1
  38. package/dist/{modal-CBEWrAi2.cjs → modal-B6591sWa.cjs} +1 -1
  39. package/dist/{modal-pPp8nia6.js → modal-DyIC3uhs.js} +1 -1
  40. package/dist/pkt-calendar.cjs +1 -1
  41. package/dist/pkt-calendar.js +1 -1
  42. package/dist/pkt-card.cjs +1 -1
  43. package/dist/pkt-card.js +1 -1
  44. package/dist/pkt-checkbox.cjs +1 -1
  45. package/dist/pkt-checkbox.js +1 -1
  46. package/dist/pkt-combobox.cjs +1 -1
  47. package/dist/pkt-combobox.js +1 -1
  48. package/dist/pkt-consent.cjs +1 -1
  49. package/dist/pkt-consent.js +1 -1
  50. package/dist/pkt-datepicker.cjs +1 -1
  51. package/dist/pkt-datepicker.js +1 -1
  52. package/dist/pkt-fileupload.cjs +1 -0
  53. package/dist/pkt-fileupload.js +2 -0
  54. package/dist/pkt-header.cjs +1 -1
  55. package/dist/pkt-header.js +1 -1
  56. package/dist/pkt-heading.cjs +1 -1
  57. package/dist/pkt-heading.js +1 -1
  58. package/dist/pkt-helptext.cjs +1 -1
  59. package/dist/pkt-helptext.js +1 -1
  60. package/dist/pkt-index.cjs +1 -1
  61. package/dist/pkt-index.js +25 -24
  62. package/dist/pkt-input-wrapper.cjs +1 -1
  63. package/dist/pkt-input-wrapper.js +1 -1
  64. package/dist/pkt-link.cjs +1 -1
  65. package/dist/pkt-link.js +1 -1
  66. package/dist/pkt-linkcard.cjs +1 -1
  67. package/dist/pkt-linkcard.js +1 -1
  68. package/dist/pkt-listbox.cjs +1 -1
  69. package/dist/pkt-listbox.js +1 -1
  70. package/dist/pkt-loader.cjs +1 -1
  71. package/dist/pkt-loader.js +1 -1
  72. package/dist/pkt-messagebox.cjs +1 -1
  73. package/dist/pkt-messagebox.js +1 -1
  74. package/dist/pkt-modal.cjs +1 -1
  75. package/dist/pkt-modal.js +1 -1
  76. package/dist/pkt-progressbar.cjs +1 -1
  77. package/dist/pkt-progressbar.js +1 -1
  78. package/dist/pkt-radiobutton.cjs +1 -1
  79. package/dist/pkt-radiobutton.js +1 -1
  80. package/dist/pkt-select.cjs +1 -1
  81. package/dist/pkt-select.js +1 -1
  82. package/dist/pkt-tabs.cjs +1 -1
  83. package/dist/pkt-tabs.js +1 -1
  84. package/dist/pkt-tag.cjs +1 -1
  85. package/dist/pkt-tag.js +1 -1
  86. package/dist/pkt-textarea.cjs +1 -1
  87. package/dist/pkt-textarea.js +1 -1
  88. package/dist/pkt-textinput.cjs +1 -1
  89. package/dist/pkt-textinput.js +1 -1
  90. package/dist/pkt-timepicker.cjs +1 -1
  91. package/dist/pkt-timepicker.js +1 -1
  92. package/dist/{progressbar-CdXtmOBb.cjs → progressbar-CrwpnIkv.cjs} +1 -1
  93. package/dist/{progressbar-Dy9AiYQz.js → progressbar-uC-0W6o4.js} +1 -1
  94. package/dist/{radiobutton-V_KPbup_.js → radiobutton-B-8-wWFf.js} +1 -1
  95. package/dist/{radiobutton-GOnpUbCW.cjs → radiobutton-DvXr7tDk.cjs} +1 -1
  96. package/dist/{select-C2i8sOk5.cjs → select-B75wzvyz.cjs} +1 -1
  97. package/dist/{select-BSAFd8Wa.js → select-D9B8Ighz.js} +3 -3
  98. package/dist/shared-types/fileupload.d.ts +73 -0
  99. package/dist/shared-types/index.d.ts +2 -0
  100. package/dist/shared-utils/fileupload/announcements.d.ts +11 -0
  101. package/dist/shared-utils/fileupload/filename.d.ts +13 -0
  102. package/dist/shared-utils/fileupload/focus-trap.d.ts +12 -0
  103. package/dist/shared-utils/fileupload/formats.d.ts +30 -0
  104. package/dist/shared-utils/fileupload/index.d.ts +15 -0
  105. package/dist/shared-utils/fileupload/navigation.d.ts +5 -0
  106. package/dist/shared-utils/fileupload/size.d.ts +12 -0
  107. package/dist/shared-utils/fileupload/transfers.d.ts +17 -0
  108. package/dist/shared-utils/fileupload/truncate.d.ts +13 -0
  109. package/dist/shared-utils/fileupload/validation.d.ts +24 -0
  110. package/dist/tests/component-registry.d.ts +2 -0
  111. package/dist/{textarea-DYOBhJrN.js → textarea-CIWXMaAD.js} +2 -2
  112. package/dist/{textarea-D7aGOyDf.cjs → textarea-CjqprL6Y.cjs} +1 -1
  113. package/dist/{textinput-DFh8_9cx.cjs → textinput-B0VWmVIF.cjs} +1 -1
  114. package/dist/{textinput-BUgntXnM.js → textinput-CxzAxM8E.js} +2 -2
  115. package/dist/{timepicker-BIfE7KKJ.cjs → timepicker-Cv9juQeB.cjs} +1 -1
  116. package/dist/{timepicker-DaSwn1FA.js → timepicker-DXrpg5C7.js} +2 -2
  117. package/package.json +2 -2
  118. package/src/components/fileupload/fileupload-base.ts +557 -0
  119. package/src/components/fileupload/fileupload-operations.ts +213 -0
  120. package/src/components/fileupload/fileupload-renderers.ts +769 -0
  121. package/src/components/fileupload/fileupload-types.ts +88 -0
  122. package/src/components/fileupload/fileupload.test.ts +949 -0
  123. package/src/components/fileupload/fileupload.ts +469 -0
  124. package/src/components/fileupload/index.ts +34 -0
  125. package/src/components/index.ts +16 -0
  126. package/src/components/input-wrapper/input-wrapper.ts +10 -3
  127. package/dist/calendar-BzEtOJla.js +0 -2031
  128. package/dist/calendar-E5jJLt8D.cjs +0 -90
  129. package/dist/input-wrapper-BRuVELpK.cjs +0 -54
  130. /package/dist/{booleanish-jJLlnr6-.cjs → booleanish-B5EFqFpJ.cjs} +0 -0
  131. /package/dist/{booleanish-BmcOU-43.js → booleanish-CRaKZ75t.js} +0 -0
  132. /package/dist/{consent-DeB_5sWH.js → consent-f_IGyHss.js} +0 -0
  133. /package/dist/{consent-hwu7pp3S.cjs → consent-zh2npq2K.cjs} +0 -0
  134. /package/dist/{dialog-polyfill.esm-1hPr0gl9.js → dialog-polyfill.esm-BmVHGRTX.js} +0 -0
  135. /package/dist/{dialog-polyfill.esm-D3Oinvk0.cjs → dialog-polyfill.esm-CXDsb3l-.cjs} +0 -0
  136. /package/dist/{heading-DiQLPxdf.js → heading-BxvE80bb.js} +0 -0
  137. /package/dist/{heading-BgSWIRii.cjs → heading-DRJTS27k.cjs} +0 -0
  138. /package/dist/{input-wrapper-Dwd-_sKm.cjs → input-wrapper-CYCCx8MP.cjs} +0 -0
  139. /package/dist/{input-wrapper-CDyHTR1V.js → input-wrapper-kw2y6Jp_.js} +0 -0
  140. /package/dist/{link-CyTCpI0k.cjs → link-B8zYc5WK.cjs} +0 -0
  141. /package/dist/{link-CKAFMz9j.js → link-CjJcwcxR.js} +0 -0
  142. /package/dist/{linkcard-BE4-FewK.cjs → linkcard-C4x-NJMi.cjs} +0 -0
  143. /package/dist/{linkcard-B0KOGaGj.js → linkcard-Ddh5R_L-.js} +0 -0
  144. /package/dist/{loader-CX2rsC01.cjs → loader-CGQXAuyM.cjs} +0 -0
  145. /package/dist/{loader-6kFZUErT.js → loader-DeizpX45.js} +0 -0
  146. /package/dist/{messagebox-Din9X53b.cjs → messagebox-Ccu29rDv.cjs} +0 -0
  147. /package/dist/{messagebox-Cwlg5sOr.js → messagebox-DqoPqo4K.js} +0 -0
  148. /package/dist/{pkt-options-controller-CXUGx134.cjs → pkt-options-controller-BPBb_O2K.cjs} +0 -0
  149. /package/dist/{pkt-options-controller-CRrI6WMk.js → pkt-options-controller-DbLrdRwH.js} +0 -0
  150. /package/dist/{repeat-Cepr3469.js → repeat-DV1O43YU.js} +0 -0
  151. /package/dist/{repeat-CqcDYBxr.cjs → repeat-DaDFvx86.cjs} +0 -0
  152. /package/dist/{tabs-Rc9N9HWa.js → tabs-4eWA7CsK.js} +0 -0
  153. /package/dist/{tabs-BfurI6C3.cjs → tabs-RnDtIFv3.cjs} +0 -0
  154. /package/dist/{tag-DLthw5b4.cjs → tag-DWtajZk6.cjs} +0 -0
  155. /package/dist/{tag-VwykmuQR.js → tag-hl59gziI.js} +0 -0
  156. /package/dist/{utils-CUqCKGvp.js → utils-BOOZ0ppt.js} +0 -0
  157. /package/dist/{utils-CjAbIttF.cjs → utils-CM67Oudc.cjs} +0 -0
@@ -4,8 +4,8 @@ import { n as s } from "./element-with-slot-4J2o3SeU.js";
4
4
  import "./icon-Co72KtgF.js";
5
5
  import { t as c } from "./if-defined-Cjj8qN-Z.js";
6
6
  import { n as l, t as u } from "./ref-RS8Uv6uC.js";
7
- import { t as d } from "./input-element-Bw1s6NlF.js";
8
- import "./input-wrapper-YfTmpSSY.js";
7
+ import { t as d } from "./input-element-CjebcQd1.js";
8
+ import "./input-wrapper-DBpMVSxt.js";
9
9
  //#region src/components/textinput/textinput.ts
10
10
  var f = class extends d {
11
11
  constructor(...e) {
@@ -1,4 +1,4 @@
1
- const e=require(`./element-DjtxO-1r.cjs`),t=require(`./class-map-CJ-msbHs.cjs`),n=require(`./element-with-slot-CWoTGA1B.cjs`);require(`./icon-Dr8sfT2X.cjs`);const r=require(`./if-defined-XKOD_t_V.cjs`),i=require(`./ref-BaJ0mBT_.cjs`),a=require(`./input-element-B1yxcfz4.cjs`);require(`./input-wrapper-BRuVELpK.cjs`);var o=e=>/^\d{2}:\d{2}$/.test(e)&&parseInt(e.slice(0,2),10)<=23&&parseInt(e.slice(3),10)<=59,s=e=>{let[t,n]=e.split(`:`).map(Number);return t*60+n},c=e=>e?Math.max(1,Math.round(e/60)):1,l=(e,t)=>{let n=e?parseInt(String(e).split(`:`)[0],10):0,r=t?parseInt(String(t).split(`:`)[0],10):23;return Array.from({length:r-n+1},(e,t)=>t+n)},u=e=>{let t=c(e);return Array.from({length:Math.floor(60/t)},(e,n)=>n*t)},d=(e,t,n,r)=>{let i=e===``?n===1?0:23:parseInt(e,10),a=t===``?n===1?0:Math.floor(59/r)*r:parseInt(t,10);return a+=n*r,a>59?(a=0,i=(i+1)%24):a<0&&(a=Math.floor(59/r)*r,i=(i-1+24)%24),{hours:String(i).padStart(2,`0`),minutes:String(a).padStart(2,`0`)}},f=class extends a.t{constructor(...e){super(...e),this.hiddenInputRef=i.t(),this.hoursInputRef=i.t(),this.minutesInputRef=i.t(),this.buttonRef=i.t(),this.value=``,this.hidePicker=!1,this.stepArrows=!1,this._hours=``,this._minutes=``,this._isOpen=!1,this._hoursDigitCount=0,this._hoursFirstDigit=-1,this._minutesDigitCount=0,this._minutesFirstDigit=-1,this._hasFocus=!1,this._outsideClickHandler=e=>{this.contains(e.target)||this._closePopup()},this._handleComponentFocusIn=()=>{this._hasFocus||(this._hasFocus=!0,this.onFocus())},this._handleComponentFocusOut=e=>{this.contains(e.relatedTarget)||(this._hasFocus=!1,this.onBlur())},this._handleHoursKeydown=e=>{let t=e.target;switch(e.key){case`ArrowUp`:{e.preventDefault();let t=this._hours===``?0:parseInt(this._hours,10);this._hours=String((t+1)%24).padStart(2,`0`),this._syncValueFromDisplay(),this.onInput();break}case`ArrowDown`:{e.preventDefault();let t=this._hours===``?0:parseInt(this._hours,10);this._hours=String((t-1+24)%24).padStart(2,`0`),this._syncValueFromDisplay(),this.onInput();break}case`ArrowRight`:e.preventDefault(),this.minutesInputRef.value?.focus();break;case`Backspace`:case`Delete`:this._hoursDigitCount=0,this._hoursFirstDigit=-1;break;case`Tab`:break;case`Enter`:{e.preventDefault();let n=this.internals.form;n?n.requestSubmit():t.blur();break}default:if(/^\d$/.test(e.key)){e.preventDefault();let t=parseInt(e.key,10);if(this._hoursDigitCount===0)this._hoursFirstDigit=t,this._hours=String(t).padStart(2,`0`),this._hoursDigitCount=1,this.onInput(),t>=3&&(this._hoursFirstDigit=-1,this._hoursDigitCount=0,this._syncValueFromDisplay(),this.minutesInputRef.value?.focus());else{let e=this._hoursFirstDigit*10+t;this._hours=e<=23?String(e).padStart(2,`0`):String(t).padStart(2,`0`),this._hoursFirstDigit=-1,this._hoursDigitCount=0,this._syncValueFromDisplay(),this.onInput(),this.minutesInputRef.value?.focus()}}else e.preventDefault()}},this._handleHoursBlur=()=>{this._hours!==``&&(this._hours=String(parseInt(this._hours,10)).padStart(2,`0`)),this._hoursDigitCount=0,this._hoursFirstDigit=-1,this._syncValueFromDisplay()},this._handleMinutesKeydown=e=>{let t=e.target;switch(e.key){case`ArrowUp`:{e.preventDefault();let t=this._minutes===``?0:parseInt(this._minutes,10);this._minutes=String((t+this._minuteStep)%60).padStart(2,`0`),this._syncValueFromDisplay(),this.onInput();break}case`ArrowDown`:{e.preventDefault();let t=this._minutes===``?0:parseInt(this._minutes,10);this._minutes=String((t-this._minuteStep+60)%60).padStart(2,`0`),this._syncValueFromDisplay(),this.onInput();break}case`ArrowLeft`:e.preventDefault(),this.hoursInputRef.value?.focus();break;case`Backspace`:case`Delete`:this._minutesDigitCount=0,this._minutesFirstDigit=-1;break;case`Tab`:break;case`Enter`:{e.preventDefault();let n=this.internals.form;n?n.requestSubmit():t.blur();break}default:if(/^\d$/.test(e.key)){e.preventDefault();let t=parseInt(e.key,10);if(this._minutesDigitCount===0)this._minutesFirstDigit=t,this._minutes=String(t).padStart(2,`0`),this._minutesDigitCount=1,this.onInput(),t>=6&&(this._minutesFirstDigit=-1,this._minutesDigitCount=0,this._syncValueFromDisplay());else{let e=this._minutesFirstDigit*10+t;this._minutes=e<=59?String(e).padStart(2,`0`):String(t).padStart(2,`0`),this._minutesFirstDigit=-1,this._minutesDigitCount=0,this._syncValueFromDisplay(),this.onInput()}}else e.preventDefault()}},this._handleMinutesBlur=()=>{this._minutes!==``&&(this._minutes=String(parseInt(this._minutes,10)).padStart(2,`0`)),this._minutesDigitCount=0,this._minutesFirstDigit=-1,this._syncValueFromDisplay()},this._handlePopupKeydown=e=>{let t=document.activeElement,n=t.closest(`.pkt-timepicker-popup__col`);if(!n)return;let r=Array.from(n.querySelectorAll(`.pkt-timepicker-popup__option`)),i=r.indexOf(t),a=t.dataset.type??``;switch(e.key){case`ArrowDown`:e.preventDefault(),this._focusOptionAndSync(r[Math.min(i+1,r.length-1)],a);break;case`ArrowUp`:e.preventDefault(),this._focusOptionAndSync(r[Math.max(i-1,0)],a);break;case`Home`:e.preventDefault(),this._focusOptionAndSync(r[0],a);break;case`End`:e.preventDefault(),this._focusOptionAndSync(r[r.length-1],a);break;case`ArrowRight`:e.preventDefault(),a===`hour`&&(this._focusOptionAndSync(t,a),this.updateComplete.then(()=>{this._scrollToSelected(),this._focusSelectedOrFirst(`minute`)}));break;case`ArrowLeft`:e.preventDefault(),a===`minute`&&(this._focusOptionAndSync(t,a),this.updateComplete.then(()=>{this._scrollToSelected(),this._focusSelectedOrFirst(`hour`)}));break;case`Enter`:case` `:e.preventDefault(),t.click();break;case`Escape`:e.preventDefault(),this._closePopup(),this.buttonRef.value?.focus();break}}}get inputRef(){return this.hiddenInputRef}manageValidity(t){let n=this.hoursInputRef.value;if(!n)return;if(this.required&&!this.value){this.internals.setValidity({valueMissing:!0},e.i.forms.messages.required,n),this._setAriaInvalid(!0);return}if(!this.value){this.internals.setValidity({}),this._setAriaInvalid(!1);return}let r=s(this.value),i=c(this.step);if(this.min&&r<s(String(this.min))){this.internals.setValidity({rangeUnderflow:!0},e.i.forms.messages.rangeUnderflowMin.replace(`{min}`,String(this.min)),n),this._setAriaInvalid(!0);return}if(this.max&&r>s(String(this.max))){this.internals.setValidity({rangeOverflow:!0},e.i.forms.messages.rangeOverflowMax.replace(`{max}`,String(this.max)),n),this._setAriaInvalid(!0);return}if(this.step&&r%i!==0){let t=i===60?e.i.forms.messages.timeStepMismatchHour:i===30?e.i.forms.messages.timeStepMismatchHalfHour:e.i.forms.messages.timeStepMismatch.replace(`{step}`,`${i}, ${i*2}, ${i*3}`);this.internals.setValidity({stepMismatch:!0},t,n),this._setAriaInvalid(!0);return}this.internals.setValidity({}),this._setAriaInvalid(!1)}_setAriaInvalid(e){if(!this.touched&&e)return;let t=this.hoursInputRef.value,n=this.minutesInputRef.value;e?(t?.setAttribute(`aria-invalid`,`true`),n?.setAttribute(`aria-invalid`,`true`)):(t?.removeAttribute(`aria-invalid`),n?.removeAttribute(`aria-invalid`))}connectedCallback(){super.connectedCallback(),this.addEventListener(`focusin`,this._handleComponentFocusIn),this.addEventListener(`focusout`,this._handleComponentFocusOut),document.addEventListener(`click`,this._outsideClickHandler)}disconnectedCallback(){super.disconnectedCallback(),this.removeEventListener(`focusin`,this._handleComponentFocusIn),this.removeEventListener(`focusout`,this._handleComponentFocusOut),document.removeEventListener(`click`,this._outsideClickHandler)}willUpdate(e){super.willUpdate(e),e.has(`value`)&&this._syncDisplayFromValue()}updated(e){super.updated(e),this.classList.toggle(`pkt-timepicker--stepper`,this.stepArrows),this.classList.toggle(`pkt-timepicker--fullwidth`,this.fullwidth),e.has(`step`)&&this.step!==null&&!this._isValidStep(this.step)&&console.warn(`pkt-timepicker: step="${this.step}" er ikke en gyldig verdi. Step må være et multiplum av 60 (hele minutter) eller nøyaktig 3600 (hel time).`)}formResetCallback(){super.formResetCallback(),this._hours=``,this._minutes=``,this._isOpen=!1}_isValidStep(e){return e===3600||e<3600&&3600%e==0&&e%60==0}_syncDisplayFromValue(){let e=o(this.value)?this.value.split(`:`):null;e?(this._hours=e[0],this._minutes=e[1]):(this._hours=``,this._minutes=``)}_syncValueFromDisplay(){if(this._hours!==``&&this._minutes!==``){let e=`${this._hours}:${this._minutes}`;e!==this.value&&(this.value=e,this.onChange(e))}else this.value!==``&&(this.value=``,this.onChange(``))}get _minuteStep(){return c(this.step)}get _hourOptions(){return l(this.min,this.max)}get _minuteOptions(){return u(this.step)}_openPopup(){this._isOpen=!0,this.updateComplete.then(()=>{this._scrollToSelected(),this._focusSelectedOrFirst(`hour`)})}_closePopup(){this._isOpen=!1,this._syncValueFromDisplay()}_togglePopup(){this._isOpen?this._closePopup():this._openPopup()}_scrollToSelected(){let e=this.querySelector(`.pkt-timepicker-popup`);e&&e.querySelectorAll(`.pkt-timepicker-popup__col`).forEach(e=>{let t=e.querySelector(`.pkt-timepicker-popup__option--selected`);t&&t.scrollIntoView({block:`center`})})}_focusSelectedOrFirst(e){let t=this.querySelector(`.pkt-timepicker-popup`);if(!t)return;let n=t.querySelectorAll(`.pkt-timepicker-popup__col`),r=e===`hour`?n[0]:n[1];if(!r)return;let i=r.querySelector(`.pkt-timepicker-popup__option--selected`),a=r.querySelector(`.pkt-timepicker-popup__option`);(i||a)?.focus()}_handleOptionClick(e,t){let n=String(e).padStart(2,`0`);t===`hour`?(this._hours=n,this.updateComplete.then(()=>this._focusSelectedOrFirst(`minute`))):(this._minutes=n,this._closePopup(),this.buttonRef.value?.focus())}_focusOptionAndSync(e,t){if(!e)return;let n=parseInt(e.dataset.value??`0`,10);t===`hour`?this._hours=String(n).padStart(2,`0`):this._minutes=String(n).padStart(2,`0`),e.focus()}_stepTime(e){let t=d(this._hours,this._minutes,e,this._minuteStep);this._hours=t.hours,this._minutes=t.minutes,this._syncValueFromDisplay()}_renderOption(n,r){let i=String(n).padStart(2,`0`),a=n===(r===`hour`?this._hours===``?NaN:parseInt(this._hours,10):this._minutes===``?NaN:parseInt(this._minutes,10));return e.d`
1
+ const e=require(`./element-DjtxO-1r.cjs`),t=require(`./class-map-CJ-msbHs.cjs`),n=require(`./element-with-slot-CWoTGA1B.cjs`);require(`./icon-Dr8sfT2X.cjs`);const r=require(`./if-defined-XKOD_t_V.cjs`),i=require(`./ref-BaJ0mBT_.cjs`),a=require(`./input-element-FGcr5hGd.cjs`);require(`./input-wrapper-B_XFi211.cjs`);var o=e=>/^\d{2}:\d{2}$/.test(e)&&parseInt(e.slice(0,2),10)<=23&&parseInt(e.slice(3),10)<=59,s=e=>{let[t,n]=e.split(`:`).map(Number);return t*60+n},c=e=>e?Math.max(1,Math.round(e/60)):1,l=(e,t)=>{let n=e?parseInt(String(e).split(`:`)[0],10):0,r=t?parseInt(String(t).split(`:`)[0],10):23;return Array.from({length:r-n+1},(e,t)=>t+n)},u=e=>{let t=c(e);return Array.from({length:Math.floor(60/t)},(e,n)=>n*t)},d=(e,t,n,r)=>{let i=e===``?n===1?0:23:parseInt(e,10),a=t===``?n===1?0:Math.floor(59/r)*r:parseInt(t,10);return a+=n*r,a>59?(a=0,i=(i+1)%24):a<0&&(a=Math.floor(59/r)*r,i=(i-1+24)%24),{hours:String(i).padStart(2,`0`),minutes:String(a).padStart(2,`0`)}},f=class extends a.t{constructor(...e){super(...e),this.hiddenInputRef=i.t(),this.hoursInputRef=i.t(),this.minutesInputRef=i.t(),this.buttonRef=i.t(),this.value=``,this.hidePicker=!1,this.stepArrows=!1,this._hours=``,this._minutes=``,this._isOpen=!1,this._hoursDigitCount=0,this._hoursFirstDigit=-1,this._minutesDigitCount=0,this._minutesFirstDigit=-1,this._hasFocus=!1,this._outsideClickHandler=e=>{this.contains(e.target)||this._closePopup()},this._handleComponentFocusIn=()=>{this._hasFocus||(this._hasFocus=!0,this.onFocus())},this._handleComponentFocusOut=e=>{this.contains(e.relatedTarget)||(this._hasFocus=!1,this.onBlur())},this._handleHoursKeydown=e=>{let t=e.target;switch(e.key){case`ArrowUp`:{e.preventDefault();let t=this._hours===``?0:parseInt(this._hours,10);this._hours=String((t+1)%24).padStart(2,`0`),this._syncValueFromDisplay(),this.onInput();break}case`ArrowDown`:{e.preventDefault();let t=this._hours===``?0:parseInt(this._hours,10);this._hours=String((t-1+24)%24).padStart(2,`0`),this._syncValueFromDisplay(),this.onInput();break}case`ArrowRight`:e.preventDefault(),this.minutesInputRef.value?.focus();break;case`Backspace`:case`Delete`:this._hoursDigitCount=0,this._hoursFirstDigit=-1;break;case`Tab`:break;case`Enter`:{e.preventDefault();let n=this.internals.form;n?n.requestSubmit():t.blur();break}default:if(/^\d$/.test(e.key)){e.preventDefault();let t=parseInt(e.key,10);if(this._hoursDigitCount===0)this._hoursFirstDigit=t,this._hours=String(t).padStart(2,`0`),this._hoursDigitCount=1,this.onInput(),t>=3&&(this._hoursFirstDigit=-1,this._hoursDigitCount=0,this._syncValueFromDisplay(),this.minutesInputRef.value?.focus());else{let e=this._hoursFirstDigit*10+t;this._hours=e<=23?String(e).padStart(2,`0`):String(t).padStart(2,`0`),this._hoursFirstDigit=-1,this._hoursDigitCount=0,this._syncValueFromDisplay(),this.onInput(),this.minutesInputRef.value?.focus()}}else e.preventDefault()}},this._handleHoursBlur=()=>{this._hours!==``&&(this._hours=String(parseInt(this._hours,10)).padStart(2,`0`)),this._hoursDigitCount=0,this._hoursFirstDigit=-1,this._syncValueFromDisplay()},this._handleMinutesKeydown=e=>{let t=e.target;switch(e.key){case`ArrowUp`:{e.preventDefault();let t=this._minutes===``?0:parseInt(this._minutes,10);this._minutes=String((t+this._minuteStep)%60).padStart(2,`0`),this._syncValueFromDisplay(),this.onInput();break}case`ArrowDown`:{e.preventDefault();let t=this._minutes===``?0:parseInt(this._minutes,10);this._minutes=String((t-this._minuteStep+60)%60).padStart(2,`0`),this._syncValueFromDisplay(),this.onInput();break}case`ArrowLeft`:e.preventDefault(),this.hoursInputRef.value?.focus();break;case`Backspace`:case`Delete`:this._minutesDigitCount=0,this._minutesFirstDigit=-1;break;case`Tab`:break;case`Enter`:{e.preventDefault();let n=this.internals.form;n?n.requestSubmit():t.blur();break}default:if(/^\d$/.test(e.key)){e.preventDefault();let t=parseInt(e.key,10);if(this._minutesDigitCount===0)this._minutesFirstDigit=t,this._minutes=String(t).padStart(2,`0`),this._minutesDigitCount=1,this.onInput(),t>=6&&(this._minutesFirstDigit=-1,this._minutesDigitCount=0,this._syncValueFromDisplay());else{let e=this._minutesFirstDigit*10+t;this._minutes=e<=59?String(e).padStart(2,`0`):String(t).padStart(2,`0`),this._minutesFirstDigit=-1,this._minutesDigitCount=0,this._syncValueFromDisplay(),this.onInput()}}else e.preventDefault()}},this._handleMinutesBlur=()=>{this._minutes!==``&&(this._minutes=String(parseInt(this._minutes,10)).padStart(2,`0`)),this._minutesDigitCount=0,this._minutesFirstDigit=-1,this._syncValueFromDisplay()},this._handlePopupKeydown=e=>{let t=document.activeElement,n=t.closest(`.pkt-timepicker-popup__col`);if(!n)return;let r=Array.from(n.querySelectorAll(`.pkt-timepicker-popup__option`)),i=r.indexOf(t),a=t.dataset.type??``;switch(e.key){case`ArrowDown`:e.preventDefault(),this._focusOptionAndSync(r[Math.min(i+1,r.length-1)],a);break;case`ArrowUp`:e.preventDefault(),this._focusOptionAndSync(r[Math.max(i-1,0)],a);break;case`Home`:e.preventDefault(),this._focusOptionAndSync(r[0],a);break;case`End`:e.preventDefault(),this._focusOptionAndSync(r[r.length-1],a);break;case`ArrowRight`:e.preventDefault(),a===`hour`&&(this._focusOptionAndSync(t,a),this.updateComplete.then(()=>{this._scrollToSelected(),this._focusSelectedOrFirst(`minute`)}));break;case`ArrowLeft`:e.preventDefault(),a===`minute`&&(this._focusOptionAndSync(t,a),this.updateComplete.then(()=>{this._scrollToSelected(),this._focusSelectedOrFirst(`hour`)}));break;case`Enter`:case` `:e.preventDefault(),t.click();break;case`Escape`:e.preventDefault(),this._closePopup(),this.buttonRef.value?.focus();break}}}get inputRef(){return this.hiddenInputRef}manageValidity(t){let n=this.hoursInputRef.value;if(!n)return;if(this.required&&!this.value){this.internals.setValidity({valueMissing:!0},e.i.forms.messages.required,n),this._setAriaInvalid(!0);return}if(!this.value){this.internals.setValidity({}),this._setAriaInvalid(!1);return}let r=s(this.value),i=c(this.step);if(this.min&&r<s(String(this.min))){this.internals.setValidity({rangeUnderflow:!0},e.i.forms.messages.rangeUnderflowMin.replace(`{min}`,String(this.min)),n),this._setAriaInvalid(!0);return}if(this.max&&r>s(String(this.max))){this.internals.setValidity({rangeOverflow:!0},e.i.forms.messages.rangeOverflowMax.replace(`{max}`,String(this.max)),n),this._setAriaInvalid(!0);return}if(this.step&&r%i!==0){let t=i===60?e.i.forms.messages.timeStepMismatchHour:i===30?e.i.forms.messages.timeStepMismatchHalfHour:e.i.forms.messages.timeStepMismatch.replace(`{step}`,`${i}, ${i*2}, ${i*3}`);this.internals.setValidity({stepMismatch:!0},t,n),this._setAriaInvalid(!0);return}this.internals.setValidity({}),this._setAriaInvalid(!1)}_setAriaInvalid(e){if(!this.touched&&e)return;let t=this.hoursInputRef.value,n=this.minutesInputRef.value;e?(t?.setAttribute(`aria-invalid`,`true`),n?.setAttribute(`aria-invalid`,`true`)):(t?.removeAttribute(`aria-invalid`),n?.removeAttribute(`aria-invalid`))}connectedCallback(){super.connectedCallback(),this.addEventListener(`focusin`,this._handleComponentFocusIn),this.addEventListener(`focusout`,this._handleComponentFocusOut),document.addEventListener(`click`,this._outsideClickHandler)}disconnectedCallback(){super.disconnectedCallback(),this.removeEventListener(`focusin`,this._handleComponentFocusIn),this.removeEventListener(`focusout`,this._handleComponentFocusOut),document.removeEventListener(`click`,this._outsideClickHandler)}willUpdate(e){super.willUpdate(e),e.has(`value`)&&this._syncDisplayFromValue()}updated(e){super.updated(e),this.classList.toggle(`pkt-timepicker--stepper`,this.stepArrows),this.classList.toggle(`pkt-timepicker--fullwidth`,this.fullwidth),e.has(`step`)&&this.step!==null&&!this._isValidStep(this.step)&&console.warn(`pkt-timepicker: step="${this.step}" er ikke en gyldig verdi. Step må være et multiplum av 60 (hele minutter) eller nøyaktig 3600 (hel time).`)}formResetCallback(){super.formResetCallback(),this._hours=``,this._minutes=``,this._isOpen=!1}_isValidStep(e){return e===3600||e<3600&&3600%e==0&&e%60==0}_syncDisplayFromValue(){let e=o(this.value)?this.value.split(`:`):null;e?(this._hours=e[0],this._minutes=e[1]):(this._hours=``,this._minutes=``)}_syncValueFromDisplay(){if(this._hours!==``&&this._minutes!==``){let e=`${this._hours}:${this._minutes}`;e!==this.value&&(this.value=e,this.onChange(e))}else this.value!==``&&(this.value=``,this.onChange(``))}get _minuteStep(){return c(this.step)}get _hourOptions(){return l(this.min,this.max)}get _minuteOptions(){return u(this.step)}_openPopup(){this._isOpen=!0,this.updateComplete.then(()=>{this._scrollToSelected(),this._focusSelectedOrFirst(`hour`)})}_closePopup(){this._isOpen=!1,this._syncValueFromDisplay()}_togglePopup(){this._isOpen?this._closePopup():this._openPopup()}_scrollToSelected(){let e=this.querySelector(`.pkt-timepicker-popup`);e&&e.querySelectorAll(`.pkt-timepicker-popup__col`).forEach(e=>{let t=e.querySelector(`.pkt-timepicker-popup__option--selected`);t&&t.scrollIntoView({block:`center`})})}_focusSelectedOrFirst(e){let t=this.querySelector(`.pkt-timepicker-popup`);if(!t)return;let n=t.querySelectorAll(`.pkt-timepicker-popup__col`),r=e===`hour`?n[0]:n[1];if(!r)return;let i=r.querySelector(`.pkt-timepicker-popup__option--selected`),a=r.querySelector(`.pkt-timepicker-popup__option`);(i||a)?.focus()}_handleOptionClick(e,t){let n=String(e).padStart(2,`0`);t===`hour`?(this._hours=n,this.updateComplete.then(()=>this._focusSelectedOrFirst(`minute`))):(this._minutes=n,this._closePopup(),this.buttonRef.value?.focus())}_focusOptionAndSync(e,t){if(!e)return;let n=parseInt(e.dataset.value??`0`,10);t===`hour`?this._hours=String(n).padStart(2,`0`):this._minutes=String(n).padStart(2,`0`),e.focus()}_stepTime(e){let t=d(this._hours,this._minutes,e,this._minuteStep);this._hours=t.hours,this._minutes=t.minutes,this._syncValueFromDisplay()}_renderOption(n,r){let i=String(n).padStart(2,`0`),a=n===(r===`hour`?this._hours===``?NaN:parseInt(this._hours,10):this._minutes===``?NaN:parseInt(this._minutes,10));return e.d`
2
2
  <button
3
3
  class=${t.t({"pkt-btn":!0,"pkt-btn--tertiary":!0,"pkt-btn--small":!0,"pkt-btn--label-only":!0,"pkt-timepicker-popup__option":!0,"pkt-timepicker-popup__option--selected":a})}
4
4
  type="button"
@@ -4,8 +4,8 @@ import { n as c } from "./element-with-slot-4J2o3SeU.js";
4
4
  import "./icon-Co72KtgF.js";
5
5
  import { t as l } from "./if-defined-Cjj8qN-Z.js";
6
6
  import { n as u, t as d } from "./ref-RS8Uv6uC.js";
7
- import { t as f } from "./input-element-Bw1s6NlF.js";
8
- import "./input-wrapper-YfTmpSSY.js";
7
+ import { t as f } from "./input-element-CjebcQd1.js";
8
+ import "./input-wrapper-DBpMVSxt.js";
9
9
  //#region ../../shared-utils/timepicker/time-utils.ts
10
10
  var p = (e) => /^\d{2}:\d{2}$/.test(e) && parseInt(e.slice(0, 2), 10) <= 23 && parseInt(e.slice(3), 10) <= 59, m = (e) => {
11
11
  let [t, n] = e.split(":").map(Number);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oslokommune/punkt-elements",
3
- "version": "16.13.2",
3
+ "version": "16.15.0",
4
4
  "description": "Komponentbiblioteket til Punkt, et designsystem laget av Oslo Origo",
5
5
  "homepage": "https://punkt.oslo.kommune.no",
6
6
  "author": "Team Designsystem, Oslo Origo",
@@ -78,5 +78,5 @@
78
78
  "url": "https://github.com/oslokommune/punkt/issues"
79
79
  },
80
80
  "license": "MIT",
81
- "gitHead": "511d6ea79105bb64a6637743f6058a7da3ca0891"
81
+ "gitHead": "5147b31531bab9ff64963a1f091a717bb93d35cf"
82
82
  }
@@ -0,0 +1,557 @@
1
+ import { PropertyValues } from 'lit'
2
+ import { property, state } from 'lit/decorators.js'
3
+
4
+ import { defaultFileUploadStrings } from 'shared-types'
5
+ import {
6
+ countFileTransferStates,
7
+ formatFileSize,
8
+ getSupportedFormatsText as sharedGetSupportedFormatsText,
9
+ isImageFileLike,
10
+ mergeFilesAndTransfers,
11
+ resolveAcceptAttribute,
12
+ validateFile as sharedValidateFile,
13
+ } from 'shared-utils/fileupload'
14
+
15
+ import { PktElement } from '@/base-elements/element'
16
+ import converters from '../../helpers/converters'
17
+
18
+ import type {
19
+ FileItem,
20
+ IPktFileUpload,
21
+ TQueueItemOperation,
22
+ TFileTransfer,
23
+ TFileUploadItemRenderer,
24
+ TFileValidateDetail,
25
+ TFilesChangedReason,
26
+ TTransferCancelledDetail,
27
+ TTransferProgress,
28
+ TUploadStrategy,
29
+ } from './fileupload-types'
30
+
31
+ export abstract class PktFileUploadBase
32
+ extends PktElement<IPktFileUpload>
33
+ implements IPktFileUpload
34
+ {
35
+ /** Unique id used for internal input + wrapper wiring. Defaults to a per-instance uuid. */
36
+ @property({ type: String }) id: string = `pkt-fileupload-${crypto.randomUUID()}`
37
+ /** Field name used by native input (`form`) or hidden fileId inputs (`custom`). */
38
+ @property({ type: String }) name: string = 'files'
39
+ /** Optional label shown by `pkt-input-wrapper`. */
40
+ @property({ type: String }) label: string = ''
41
+ /** Optional help text shown under label. */
42
+ @property({ type: String }) helptext: string = ''
43
+ /** Mark input as required in form strategy. */
44
+ @property({ type: Boolean, reflect: true }) required = false
45
+ /** Allow selecting/dropping multiple files. */
46
+ @property({ type: Boolean, reflect: true }) multiple = false
47
+ /** Disable all interaction in drop zone + queue actions. */
48
+ @property({ type: Boolean, reflect: true }) disabled = false
49
+ /** Stretch component width to container. */
50
+ @property({ type: Boolean, reflect: true, attribute: 'fullwidth' }) fullwidth = false
51
+ /** Upload mode: `form` uses native file submit, `custom` emits upload request events per file. */
52
+ @property({ type: String, reflect: true, attribute: 'upload-strategy' })
53
+ uploadStrategy: TUploadStrategy = 'form'
54
+ /** Queue visual mode (`filename` or `thumbnail`). */
55
+ @property({ type: String, reflect: true, attribute: 'item-renderer' })
56
+ itemRenderer: TFileUploadItemRenderer = 'filename'
57
+ /** Native file input accept hint (browser picker filtering). */
58
+ @property({ type: String }) accept: string = ''
59
+ /** Built-in format validation source (csv attribute or array property). */
60
+ @property({ converter: converters.csvToArray, attribute: 'allowed-formats' })
61
+ allowedFormats: string[] = []
62
+ /** Optional custom format validation message. Supports `{formats}` placeholder. */
63
+ @property({ type: String, attribute: 'format-error-message' }) formatErrorMessage = ''
64
+ /** Max allowed file size (bytes or string like `500KB`, `5MB`). */
65
+ @property({ attribute: 'max-file-size' }) maxFileSize?: string | number
66
+ /** Optional custom size validation message. Supports `{maxSize}` placeholder. */
67
+ @property({ type: String, attribute: 'size-error-message' }) sizeErrorMessage = ''
68
+ /** Optional JS callback for custom validation. Property-only on purpose (not HTML-attribute friendly). */
69
+ @property({ attribute: false }) onFileValidation?: (file: File) => string | null
70
+ /** Transfer state list keyed by `fileId` (used for custom upload progress/error/cancel UI). */
71
+ @property({ attribute: false }) transfers: TFileTransfer[] = []
72
+ /** Enables built-in comment operation in queue items (disabled in thumbnail view for parity with React). */
73
+ @property({ type: Boolean, attribute: 'add-comments-enabled' }) addCommentsEnabled = false
74
+ /** Enables built-in rename operation in queue items (disabled in thumbnail view for parity with React). */
75
+ @property({ type: Boolean, attribute: 'rename-files-enabled' }) renameFilesEnabled = false
76
+ /** Custom queue operations (JS property only). Supports inline + expanded operation UIs. */
77
+ @property({ attribute: false }) extraOperations: TQueueItemOperation[] = []
78
+ /** Toggle image thumbnail behavior in queue when renderer supports it. */
79
+ @property({ type: Boolean, reflect: true, attribute: 'enable-image-preview' })
80
+ enableImagePreview = false
81
+ /** External error flag — combines with internal validation errors. */
82
+ @property({ type: Boolean, attribute: 'has-error' }) hasError = false
83
+ /** External error message shown in the alert under the drop zone. */
84
+ @property({ type: String, attribute: 'error-message' }) errorMessage = ''
85
+ /** Show "Valgfritt" tag in the input wrapper. */
86
+ @property({ type: Boolean, attribute: 'optional-tag' }) optionalTag = false
87
+ /** Show "Må fylles ut" tag in the input wrapper. */
88
+ @property({ type: Boolean, attribute: 'required-tag' }) requiredTag = false
89
+ /** Trailing characters to keep when middle-truncating long filenames. Set to `0` to disable. */
90
+ @property({ type: Number, attribute: 'truncate-tail' }) truncateTail: number = 4
91
+ /** Controlled mode source of truth. Parent owns file list and updates this prop from events. */
92
+ @property({ attribute: false }) value?: FileItem[]
93
+ /** Uncontrolled initial file list. Used once during first initialization. */
94
+ @property({ attribute: false }) defaultValue?: FileItem[]
95
+
96
+ @state() protected files: FileItem[] = []
97
+ @state() protected isDragActive = false
98
+ @state() protected validationErrorMessage: string | null = null
99
+ @state() protected addedAnnouncement = ''
100
+ @state() private thumbnailUrls: Record<string, string> = {}
101
+
102
+ private hasInitializedValue = false
103
+ private hasWarnedInvalidValueCombo = false
104
+ private thumbnailFileById = new Map<string, File>()
105
+ private connectedForm: HTMLFormElement | null = null
106
+ private addedAnnouncementTimer: ReturnType<typeof setTimeout> | null = null
107
+ private readonly requiredSelectionMessage = defaultFileUploadStrings.requiredMissing
108
+
109
+ protected get isControlled() {
110
+ return this.value !== undefined
111
+ }
112
+
113
+ /** True if the component itself wants to show an error (internal validation or external prop). */
114
+ protected get hasEffectiveError() {
115
+ return !!this.validationErrorMessage || this.hasError || !!this.errorMessage.trim()
116
+ }
117
+
118
+ /** Message to show in the alert under the drop zone. Internal validation takes priority. */
119
+ protected get effectiveErrorMessage(): string {
120
+ return this.validationErrorMessage ?? this.errorMessage ?? ''
121
+ }
122
+
123
+ protected get hasValidationError() {
124
+ return !!this.validationErrorMessage
125
+ }
126
+
127
+ protected firstUpdated(changedProperties: PropertyValues): void {
128
+ super.firstUpdated?.(changedProperties)
129
+ if (!this.hasInitializedValue) {
130
+ this.files = this.value ?? this.defaultValue ?? []
131
+ this.syncThumbnailUrls(this.files)
132
+ this.hasInitializedValue = true
133
+ }
134
+ if (this.value !== undefined && this.defaultValue !== undefined) {
135
+ this.warnInvalidValueCombo()
136
+ }
137
+ }
138
+
139
+ protected willUpdate(changedProperties: PropertyValues): void {
140
+ super.willUpdate(changedProperties)
141
+ if (!this.hasInitializedValue) return
142
+
143
+ if (
144
+ this.isControlled &&
145
+ (changedProperties.has('value') ||
146
+ changedProperties.has('itemRenderer') ||
147
+ changedProperties.has('enableImagePreview'))
148
+ ) {
149
+ this.files = this.value ?? []
150
+ this.syncThumbnailUrls(this.files)
151
+ }
152
+ }
153
+
154
+ protected updated(changedProperties: PropertyValues): void {
155
+ super.updated(changedProperties)
156
+ if (
157
+ changedProperties.has('uploadStrategy') ||
158
+ changedProperties.has('required') ||
159
+ changedProperties.has('value')
160
+ ) {
161
+ this.syncFormSubmitListener()
162
+ }
163
+ if (
164
+ this.uploadStrategy === 'form' &&
165
+ (changedProperties.has('value') ||
166
+ changedProperties.has('files') ||
167
+ changedProperties.has('uploadStrategy'))
168
+ ) {
169
+ this.syncNativeInputFiles()
170
+ }
171
+ }
172
+
173
+ private syncNativeInputFiles() {
174
+ const input = this.querySelector<HTMLInputElement>(`#${this.id}-input`)
175
+ if (!input || typeof DataTransfer === 'undefined') return
176
+ try {
177
+ const dataTransfer = new DataTransfer()
178
+ for (const item of this.getCurrentFiles()) {
179
+ if (item.file) dataTransfer.items.add(item.file)
180
+ }
181
+ input.files = dataTransfer.files
182
+ } catch {
183
+ // jsdom and some test environments do not implement DataTransfer; not critical.
184
+ }
185
+ }
186
+
187
+ private warnInvalidValueCombo() {
188
+ if (this.hasWarnedInvalidValueCombo) return
189
+
190
+ console.warn(
191
+ 'PktFileUpload: Både value og defaultValue er angitt. Komponenten kan være enten kontrollert eller ukontrollert, ikke begge. value vil bli prioritert.',
192
+ )
193
+ this.hasWarnedInvalidValueCombo = true
194
+ }
195
+
196
+ disconnectedCallback(): void {
197
+ this.teardownFormSubmitListener()
198
+ if (this.addedAnnouncementTimer) {
199
+ clearTimeout(this.addedAnnouncementTimer)
200
+ this.addedAnnouncementTimer = null
201
+ }
202
+ super.disconnectedCallback()
203
+ this.revokeAllThumbnailUrls()
204
+ }
205
+
206
+ private setAddedAnnouncement(message: string) {
207
+ this.addedAnnouncement = message
208
+ if (this.addedAnnouncementTimer) clearTimeout(this.addedAnnouncementTimer)
209
+ this.addedAnnouncementTimer = setTimeout(() => {
210
+ this.addedAnnouncement = ''
211
+ this.addedAnnouncementTimer = null
212
+ }, 1500)
213
+ }
214
+
215
+ connectedCallback(): void {
216
+ super.connectedCallback()
217
+ this.syncFormSubmitListener()
218
+ }
219
+
220
+ protected onNativeFileChange = (event: Event) => {
221
+ const target = event.target as HTMLInputElement
222
+ this.addFiles(target.files ? Array.from(target.files) : [])
223
+ // In custom mode we do not submit native file input values, so we can clear the input
224
+ // to allow selecting the same file again. In form mode we keep native files for form submit.
225
+ if (this.uploadStrategy === 'custom') {
226
+ target.value = ''
227
+ }
228
+ }
229
+
230
+ protected openFileDialog = (event: Event) => {
231
+ event.preventDefault()
232
+ event.stopPropagation()
233
+ if (this.disabled) return
234
+ const input = this.querySelector<HTMLInputElement>(`#${this.id}-input`)
235
+ input?.click()
236
+ }
237
+
238
+ protected onDropZoneClick = (event: MouseEvent) => {
239
+ if (this.disabled) return
240
+ const target = event.target as HTMLElement
241
+ if (target.closest('.pkt-fileupload__drop-zone__placeholder__title__open-file-dialog')) return
242
+ const input = this.querySelector<HTMLInputElement>(`#${this.id}-input`)
243
+ input?.click()
244
+ }
245
+
246
+ protected onDragOver = (event: DragEvent) => {
247
+ event.preventDefault()
248
+ if (this.disabled) return
249
+ this.isDragActive = true
250
+ }
251
+
252
+ protected onDragLeave = () => {
253
+ this.isDragActive = false
254
+ }
255
+
256
+ protected onDrop = (event: DragEvent) => {
257
+ event.preventDefault()
258
+ this.isDragActive = false
259
+ if (this.disabled) return
260
+ const droppedFiles = event.dataTransfer?.files ? Array.from(event.dataTransfer.files) : []
261
+ this.addFiles(droppedFiles)
262
+ }
263
+
264
+ protected addFiles(selectedFiles: File[]) {
265
+ if (selectedFiles.length === 0) return
266
+ const normalizedFiles = this.multiple ? selectedFiles : [selectedFiles[0]]
267
+
268
+ for (const file of normalizedFiles) {
269
+ const validationError = this.validateFile(file)
270
+ if (validationError) {
271
+ this.validationErrorMessage = validationError
272
+ return
273
+ }
274
+ }
275
+
276
+ this.validationErrorMessage = null
277
+ const newItems: FileItem[] = normalizedFiles.map((file) => ({
278
+ fileId: crypto.randomUUID(),
279
+ file,
280
+ attributes: {
281
+ targetFilename: file.name,
282
+ },
283
+ }))
284
+
285
+ this.setAddedAnnouncement(
286
+ newItems.length === 1
287
+ ? defaultFileUploadStrings.srFileAdded(newItems[0].attributes.targetFilename)
288
+ : defaultFileUploadStrings.srFilesAdded(newItems.length),
289
+ )
290
+
291
+ const currentFiles = this.getCurrentFiles()
292
+ const nextFiles = this.multiple ? [...currentFiles, ...newItems] : [newItems[0]]
293
+ this.commitFiles(
294
+ nextFiles,
295
+ 'add',
296
+ newItems.map((item) => item.fileId),
297
+ )
298
+
299
+ if (this.uploadStrategy === 'custom') {
300
+ newItems.forEach((fileItem) => {
301
+ this.dispatchEvent(
302
+ new CustomEvent('file-upload-requested', {
303
+ detail: {
304
+ fileId: fileItem.fileId,
305
+ file: fileItem.file,
306
+ attributes: fileItem.attributes,
307
+ },
308
+ bubbles: true,
309
+ composed: true,
310
+ }),
311
+ )
312
+ })
313
+ }
314
+ }
315
+
316
+ protected getCurrentFiles(): FileItem[] {
317
+ return this.isControlled ? (this.value ?? []) : this.files
318
+ }
319
+
320
+ protected cancelTransfer(fileId: string) {
321
+ if (this.disabled) return
322
+ const currentItem = this.getCurrentFiles().find((item) => item.fileId === fileId)
323
+ if (!currentItem) return
324
+
325
+ this.removeFileItem(fileId)
326
+
327
+ // CE-native deviation from React callback props: emit a bubbling event for host-driven cancel/delete logic.
328
+ this.dispatchEvent(
329
+ new CustomEvent<TTransferCancelledDetail>('transfer-cancelled', {
330
+ detail: {
331
+ fileId: currentItem.fileId,
332
+ file: currentItem.file,
333
+ attributes: currentItem.attributes,
334
+ },
335
+ bubbles: true,
336
+ composed: true,
337
+ }),
338
+ )
339
+ }
340
+
341
+ protected removeFileItem(fileId: string) {
342
+ if (this.disabled) return
343
+ const currentFiles = this.getCurrentFiles()
344
+ const nextFiles = currentFiles.filter((item) => item.fileId !== fileId)
345
+ if (nextFiles.length === currentFiles.length) return
346
+ this.commitFiles(nextFiles, 'remove', [fileId])
347
+ }
348
+
349
+ // CE-native deviation: this is a method-based update hook instead of React callback props.
350
+ // It keeps queue lifecycle explicit without introducing React-specific abstractions.
351
+ public updateFileItem(fileId: string, updates: Partial<FileItem>) {
352
+ const currentFiles = this.getCurrentFiles()
353
+ const nextFiles = currentFiles.map((item) =>
354
+ item.fileId === fileId ? { ...item, ...updates } : item,
355
+ )
356
+ this.commitFiles(nextFiles, 'update', [fileId])
357
+ }
358
+
359
+ protected commitFiles(
360
+ nextFiles: FileItem[],
361
+ reason: TFilesChangedReason,
362
+ changedFileIds?: string[],
363
+ ) {
364
+ if (!this.isControlled) {
365
+ this.files = nextFiles
366
+ this.syncThumbnailUrls(nextFiles)
367
+ }
368
+ if (this.validationErrorMessage === this.requiredSelectionMessage && nextFiles.length > 0) {
369
+ this.validationErrorMessage = null
370
+ }
371
+ this.dispatchEvent(
372
+ new CustomEvent('files-changed', {
373
+ detail: { files: nextFiles, reason, changedFileIds },
374
+ bubbles: true,
375
+ composed: true,
376
+ }),
377
+ )
378
+ }
379
+
380
+ protected getThumbnailUrl(file: FileItem): string | undefined {
381
+ return this.thumbnailUrls[file.fileId]
382
+ }
383
+
384
+ protected getTransferProgress(fileId: string): TTransferProgress {
385
+ const transfer = this.transfers.find((item) => item.fileId === fileId)
386
+ if (transfer) return transfer.progress
387
+ return this.uploadStrategy === 'form' ? 'done' : 'queued'
388
+ }
389
+
390
+ protected getTransferForFile(fileId: string): TFileTransfer | undefined {
391
+ return this.transfers.find((item) => item.fileId === fileId)
392
+ }
393
+
394
+ protected isImageFile(file: FileItem): boolean {
395
+ return isImageFileLike(file)
396
+ }
397
+
398
+ protected formatFileSize(bytes: number): string {
399
+ return formatFileSize(bytes)
400
+ }
401
+
402
+ protected getResolvedAcceptValue(): string {
403
+ return resolveAcceptAttribute(this.accept, this.allowedFormats)
404
+ }
405
+
406
+ protected getSupportedFormatsText(): string {
407
+ return sharedGetSupportedFormatsText(this.allowedFormats, this.accept)
408
+ }
409
+
410
+ protected getSortedFilesAndTransfers() {
411
+ return mergeFilesAndTransfers(this.getCurrentFiles(), this.transfers, this.uploadStrategy)
412
+ }
413
+
414
+ private validateFile(file: File): string | null {
415
+ const baseError = sharedValidateFile(file, {
416
+ allowedFormats: this.allowedFormats,
417
+ maxFileSize: this.maxFileSize,
418
+ formatErrorMessage: this.formatErrorMessage,
419
+ sizeErrorMessage: this.sizeErrorMessage,
420
+ onFileValidation: this.onFileValidation,
421
+ })
422
+ if (baseError) return baseError
423
+
424
+ const eventDetail: TFileValidateDetail = { file, errorMessage: null }
425
+ // CE-native deviation from React: callbacks cannot be passed declaratively in HTML,
426
+ // so we expose an event hook where consumers can set detail.errorMessage.
427
+ const validationEvent = new CustomEvent<TFileValidateDetail>('file-validate', {
428
+ detail: eventDetail,
429
+ bubbles: true,
430
+ composed: true,
431
+ cancelable: true,
432
+ })
433
+ this.dispatchEvent(validationEvent)
434
+
435
+ if (eventDetail.errorMessage) return eventDetail.errorMessage
436
+ if (validationEvent.defaultPrevented) return defaultFileUploadStrings.genericValidationRejection
437
+ return null
438
+ }
439
+
440
+ private syncThumbnailUrls(nextFiles: FileItem[]) {
441
+ const supportsObjectUrl =
442
+ typeof URL !== 'undefined' &&
443
+ typeof URL.createObjectURL === 'function' &&
444
+ typeof URL.revokeObjectURL === 'function'
445
+
446
+ if (!supportsObjectUrl) {
447
+ this.thumbnailUrls = {}
448
+ this.thumbnailFileById.clear()
449
+ return
450
+ }
451
+
452
+ const nextIds = new Set(nextFiles.map((item) => item.fileId))
453
+ const nextUrlMap: Record<string, string> = {}
454
+
455
+ for (const [fileId, url] of Object.entries(this.thumbnailUrls)) {
456
+ if (!nextIds.has(fileId)) {
457
+ URL.revokeObjectURL(url)
458
+ this.thumbnailFileById.delete(fileId)
459
+ }
460
+ }
461
+
462
+ for (const item of nextFiles) {
463
+ const previousUrl = this.thumbnailUrls[item.fileId]
464
+ const previousFile = this.thumbnailFileById.get(item.fileId)
465
+
466
+ if (!item.file || !this.isImageFile(item)) {
467
+ if (previousUrl) {
468
+ URL.revokeObjectURL(previousUrl)
469
+ this.thumbnailFileById.delete(item.fileId)
470
+ }
471
+ continue
472
+ }
473
+
474
+ if (previousUrl && previousFile === item.file) {
475
+ nextUrlMap[item.fileId] = previousUrl
476
+ continue
477
+ }
478
+
479
+ if (previousUrl && previousFile !== item.file) {
480
+ URL.revokeObjectURL(previousUrl)
481
+ }
482
+
483
+ const nextUrl = URL.createObjectURL(item.file)
484
+ this.thumbnailFileById.set(item.fileId, item.file)
485
+ nextUrlMap[item.fileId] = nextUrl
486
+ }
487
+
488
+ this.thumbnailUrls = nextUrlMap
489
+ }
490
+
491
+ private revokeAllThumbnailUrls() {
492
+ if (typeof URL === 'undefined' || typeof URL.revokeObjectURL !== 'function') {
493
+ this.thumbnailUrls = {}
494
+ this.thumbnailFileById.clear()
495
+ return
496
+ }
497
+
498
+ Object.values(this.thumbnailUrls).forEach((url) => URL.revokeObjectURL(url))
499
+ this.thumbnailUrls = {}
500
+ this.thumbnailFileById.clear()
501
+ }
502
+
503
+ /** Polite summary: prefer the transient "file added" announcement, then fall back to upload completion. */
504
+ protected getSrUploadedMessage(): string {
505
+ if (this.addedAnnouncement) return this.addedAnnouncement
506
+ const { totalCount, uploadedCount } = countFileTransferStates(
507
+ this.getCurrentFiles(),
508
+ this.transfers,
509
+ )
510
+ if (totalCount === 0 || uploadedCount === 0) return ''
511
+ return defaultFileUploadStrings.srFilesUploadedOfTotal(
512
+ uploadedCount,
513
+ totalCount,
514
+ defaultFileUploadStrings.fileLabel(totalCount),
515
+ )
516
+ }
517
+
518
+ /** Assertive summary for transfer errors (parity with React `FileUpload`). */
519
+ protected getSrErrorsMessage(): string {
520
+ const { totalCount, failedCount } = countFileTransferStates(
521
+ this.getCurrentFiles(),
522
+ this.transfers,
523
+ )
524
+ if (totalCount === 0 || failedCount === 0) return ''
525
+ return defaultFileUploadStrings.srFilesFailedOfTotal(
526
+ failedCount,
527
+ totalCount,
528
+ defaultFileUploadStrings.fileLabel(totalCount),
529
+ )
530
+ }
531
+
532
+ private syncFormSubmitListener() {
533
+ const nextForm = this.closest('form')
534
+ if (this.connectedForm === nextForm) return
535
+
536
+ this.teardownFormSubmitListener()
537
+ this.connectedForm = nextForm
538
+
539
+ if (this.connectedForm) {
540
+ this.connectedForm.addEventListener('submit', this.onHostFormSubmit)
541
+ }
542
+ }
543
+
544
+ private teardownFormSubmitListener() {
545
+ if (!this.connectedForm) return
546
+ this.connectedForm.removeEventListener('submit', this.onHostFormSubmit)
547
+ this.connectedForm = null
548
+ }
549
+
550
+ private onHostFormSubmit = (event: Event) => {
551
+ if (!this.required || this.uploadStrategy !== 'custom') return
552
+ if (this.getCurrentFiles().length > 0) return
553
+
554
+ this.validationErrorMessage = this.requiredSelectionMessage
555
+ event.preventDefault()
556
+ }
557
+ }