@stimulus-plumbers/controllers 0.2.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.
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/stimulus-plumbers-controllers.es.js +1459 -0
- package/dist/stimulus-plumbers-controllers.umd.js +1 -0
- package/package.json +57 -0
- package/src/aria.js +173 -0
- package/src/controllers/auto_resize_controller.js +17 -0
- package/src/controllers/calendar_month_controller.js +100 -0
- package/src/controllers/calendar_month_observer_controller.js +24 -0
- package/src/controllers/datepicker_controller.js +101 -0
- package/src/controllers/dismisser_controller.js +10 -0
- package/src/controllers/flipper_controller.js +33 -0
- package/src/controllers/form-field_controller.js +77 -0
- package/src/controllers/modal_controller.js +104 -0
- package/src/controllers/panner_controller.js +10 -0
- package/src/controllers/password_reveal_controller.js +9 -0
- package/src/controllers/popover_controller.js +76 -0
- package/src/controllers/visibility_controller.js +32 -0
- package/src/focus.js +128 -0
- package/src/index.js +23 -0
- package/src/keyboard.js +92 -0
- package/src/plumbers/calendar.js +399 -0
- package/src/plumbers/content_loader.js +134 -0
- package/src/plumbers/dismisser.js +82 -0
- package/src/plumbers/flipper.js +272 -0
- package/src/plumbers/index.js +10 -0
- package/src/plumbers/plumber/index.js +110 -0
- package/src/plumbers/plumber/support.js +101 -0
- package/src/plumbers/shifter.js +164 -0
- package/src/plumbers/visibility.js +136 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(o,d){typeof exports=="object"&&typeof module<"u"?d(exports,require("@hotwired/stimulus")):typeof define=="function"&&define.amd?define(["exports","@hotwired/stimulus"],d):(o=typeof globalThis<"u"?globalThis:o||self,d(o.StimulusPlumbersControllers={},o.Stimulus))})(this,(function(o,d){"use strict";const M=["a[href]","area[href]","button:not([disabled])","input:not([disabled])","select:not([disabled])","textarea:not([disabled])",'[tabindex]:not([tabindex="-1"])',"audio[controls]","video[controls]",'[contenteditable]:not([contenteditable="false"])'].join(",");function T(s){return Array.from(s.querySelectorAll(M)).filter(t=>w(t))}function w(s){return!!(s.offsetWidth||s.offsetHeight||s.getClientRects().length)}function k(s){const t=T(s);return t.length>0?(t[0].focus(),!0):!1}class x{constructor(t,e={}){this.container=t,this.previouslyFocused=null,this.options=e,this.isActive=!1}activate(){this.isActive||(this.previouslyFocused=document.activeElement,this.isActive=!0,this.options.initialFocus?this.options.initialFocus.focus():k(this.container),this.container.addEventListener("keydown",this.handleKeyDown))}deactivate(){if(!this.isActive)return;this.isActive=!1,this.container.removeEventListener("keydown",this.handleKeyDown);const t=this.options.returnFocus||this.previouslyFocused;t&&w(t)&&t.focus()}handleKeyDown=t=>{if(t.key==="Escape"&&this.options.escapeDeactivates){t.preventDefault(),this.deactivate();return}if(t.key!=="Tab")return;const e=T(this.container);if(e.length===0)return;const i=e[0],n=e[e.length-1];t.shiftKey&&document.activeElement===i?(t.preventDefault(),n.focus()):!t.shiftKey&&document.activeElement===n&&(t.preventDefault(),i.focus())}}class q{constructor(){this.savedElement=null}save(){this.savedElement=document.activeElement}restore(){this.savedElement&&w(this.savedElement)&&(this.savedElement.focus(),this.savedElement=null)}}function B(s,t){return s.key===t}function K(s){return s.key==="Enter"||s.key===" "}function U(s){return["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(s.key)}function z(s){s.preventDefault(),s.stopPropagation()}class X{constructor(t,e=0){this.items=t,this.currentIndex=e,this.updateTabIndex()}handleKeyDown(t){let e;switch(t.key){case"ArrowDown":case"ArrowRight":t.preventDefault(),e=(this.currentIndex+1)%this.items.length;break;case"ArrowUp":case"ArrowLeft":t.preventDefault(),e=this.currentIndex===0?this.items.length-1:this.currentIndex-1;break;case"Home":t.preventDefault(),e=0;break;case"End":t.preventDefault(),e=this.items.length-1;break;default:return}this.setCurrentIndex(e)}setCurrentIndex(t){t>=0&&t<this.items.length&&(this.currentIndex=t,this.updateTabIndex(),this.items[t].focus())}updateTabIndex(){this.items.forEach((t,e)=>{t.tabIndex=e===this.currentIndex?0:-1})}updateItems(t){this.items=t,this.currentIndex=Math.min(this.currentIndex,t.length-1),this.updateTabIndex()}}const G=(s,t,e)=>{let i=document.querySelector(`[data-live-region="${s}"]`);return i||(i=document.createElement("div"),i.className="sr-only",i.dataset.liveRegion=s,i.setAttribute("aria-live",s),i.setAttribute("aria-atomic",t.toString()),i.setAttribute("aria-relevant",e),document.body.appendChild(i)),i};function D(s,t={}){const{politeness:e="polite",atomic:i=!0,relevant:n="additions text"}=t,a=G(e,i,n);a.textContent="",setTimeout(()=>{a.textContent=s},100)}const E=(s="a11y")=>`${s}-${Math.random().toString(36).substr(2,9)}`,J=(s,t="element")=>s.id||(s.id=E(t)),b=(s,t,e)=>{s.setAttribute(t,e.toString())},Q=(s,t)=>b(s,"aria-expanded",t),Z=(s,t)=>b(s,"aria-pressed",t),tt=(s,t)=>b(s,"aria-checked",t);function et(s,t){b(s,"aria-disabled",t),t?s.setAttribute("tabindex","-1"):s.removeAttribute("tabindex")}const A={menu:"menu",listbox:"listbox",tree:"tree",grid:"grid",dialog:"dialog"},L=(s,t,e)=>{Object.entries(t).forEach(([i,n])=>{s.setAttribute(i,n),e[i]=n})},v=(s,t,e)=>e||!s.hasAttribute(t);function F({trigger:s,target:t,role:e=null,override:i=!1}){const n={trigger:{},target:{}};if(!s||!t)return n;const a={},r={};if(e&&v(t,"role",i)&&(r.role=e),t.id&&(v(s,"aria-controls",i)&&(a["aria-controls"]=t.id),e==="tooltip"&&v(s,"aria-describedby",i)&&(a["aria-describedby"]=t.id)),e&&v(s,"aria-haspopup",i)){const l=A[e]||"true";a["aria-haspopup"]=l}return L(t,r,n.target),L(s,a,n.trigger),n}const S=(s,t)=>{t.forEach(e=>{s.hasAttribute(e)&&s.removeAttribute(e)})};function it({trigger:s,target:t,attributes:e=null}){if(!s||!t)return;S(s,e||["aria-controls","aria-haspopup","aria-describedby"]),(!e||e.includes("role"))&&S(t,["role"])}const I={get visibleOnly(){return!0},hiddenClass:null},V={get top(){return"bottom"},get bottom(){return"top"},get left(){return"right"},get right(){return"left"}};function u({x:s,y:t,width:e,height:i}){return{x:s,y:t,width:e,height:i,left:s,right:s+e,top:t,bottom:t+i}}function O(){return u({x:0,y:0,width:window.innerWidth||document.documentElement.clientWidth,height:window.innerHeight||document.documentElement.clientHeight})}function st(s){if(!(s instanceof HTMLElement))return!1;const t=O(),e=s.getBoundingClientRect(),i=e.top<=t.height&&e.top+e.height>0,n=e.left<=t.width&&e.left+e.width>0;return i&&n}function y(s){return s instanceof Date&&!isNaN(s)}function g(...s){if(s.length===0)throw"Missing values to parse as date";if(s.length===1){const t=new Date(s[0]);if(s[0]&&y(t))return t}else{const t=new Date(...s);if(y(t))return t}}const nt={element:null,visible:null,dispatch:!0,prefix:""};class p{constructor(t,e={}){this.controller=t;const i=Object.assign({},nt,e),{element:n,visible:a,dispatch:r,prefix:l}=i;this.element=n||t.element,this.visibleOnly=typeof a=="boolean"?a:I.visibleOnly,this.visibleCallback=typeof a=="string"?a:null,this.notify=!!r,this.prefix=typeof l=="string"&&l?l:t.identifier}get visible(){return this.element instanceof HTMLElement?this.visibleOnly?st(this.element)&&this.isVisible(this.element):!0:!1}isVisible(t){if(this.visibleCallback){const e=this.findCallback(this.visibleCallback);if(typeof e=="function")return e(t)}return t instanceof HTMLElement?!t.hasAttribute("hidden"):!1}dispatch(t,{target:e=null,prefix:i=null,detail:n=null}={}){if(this.notify)return this.controller.dispatch(t,{target:e||this.element,prefix:i||this.prefix,detail:n})}findCallback(t){if(typeof t!="string")return;const e=this,i=t.split(".").reduce((a,r)=>a&&a[r],e.controller);if(typeof i=="function")return i.bind(e.controller);const n=t.split(".").reduce((a,r)=>a&&a[r],e);if(typeof n=="function")return n.bind(e)}async awaitCallback(t,...e){if(typeof t=="string"&&(t=this.findCallback(t)),typeof t=="function"){const i=t(...e);return i instanceof Promise?await i:i}}}const R=7,W={locales:["default"],today:"",day:null,month:null,year:null,since:null,till:null,disabledDates:[],disabledWeekdays:[],disabledDays:[],disabledMonths:[],disabledYears:[],firstDayOfWeek:0,onNavigated:"navigated"};class at extends p{constructor(t,e={}){super(t,e);const i=Object.assign({},W,e),{onNavigated:n,since:a,till:r,firstDayOfWeek:l}=i;this.onNavigated=n,this.since=g(a),this.till=g(r),this.firstDayOfWeek=0<=l&&l<7?l:W.firstDayOfWeek;const{disabledDates:h,disabledWeekdays:f,disabledDays:c,disabledMonths:m,disabledYears:P}=i;this.disabledDates=Array.isArray(h)?h:[],this.disabledWeekdays=Array.isArray(f)?f:[],this.disabledDays=Array.isArray(c)?c:[],this.disabledMonths=Array.isArray(m)?m:[],this.disabledYears=Array.isArray(P)?P:[];const{today:Et,day:_,month:$,year:N}=i;this.now=g(Et)||new Date,typeof N=="number"&&typeof $=="number"&&typeof _=="number"?this.current=g(N,$,_):this.current=this.now,this.build(),this.enhance()}build(){this.daysOfWeek=this.buildDaysOfWeek(),this.daysOfMonth=this.buildDaysOfMonth(),this.monthsOfYear=this.buildMonthsOfYear()}buildDaysOfWeek(){const t=new Intl.DateTimeFormat(this.localesValue,{weekday:"long"}),e=new Intl.DateTimeFormat(this.localesValue,{weekday:"short"}),i=new Date("2024-10-06"),n=[];for(let a=this.firstDayOfWeek,r=a+7;a<r;a++){const l=new Date(i);l.setDate(i.getDate()+a),n.push({date:l,value:l.getDay(),long:t.format(l),short:e.format(l)})}return n}buildDaysOfMonth(){const t=this.month,e=this.year,i=[],n=c=>({current:this.month===c.getMonth()&&this.year===c.getFullYear(),date:c,value:c.getDate(),month:c.getMonth(),year:c.getFullYear(),iso:c.toISOString()}),a=new Date(e,t).getDay(),r=this.firstDayOfWeek-a;for(let c=r>0?r-7:r;c<0;c++){const m=new Date(e,t,c+1);i.push(n(m))}const l=new Date(e,t+1,0).getDate();for(let c=1;c<=l;c++){const m=new Date(e,t,c);i.push(n(m))}const h=i.length%R,f=h===0?0:R-h;for(let c=1;c<=f;c++){const m=new Date(e,t+1,c);i.push(n(m))}return i}buildMonthsOfYear(){const t=new Intl.DateTimeFormat(this.localesValue,{month:"long"}),e=new Intl.DateTimeFormat(this.localesValue,{month:"short"}),i=new Intl.DateTimeFormat(this.localesValue,{month:"numeric"}),n=[];for(let a=0;a<12;a++){const r=new Date(this.year,a);n.push({date:r,value:r.getMonth(),long:t.format(r),short:e.format(r),numeric:i.format(r)})}return n}get today(){return this.now}set today(t){if(!y(t))return;const e=this.month?this.month:t.getMonth(),i=this.year?this.year:t.getFullYear(),n=e==t.getMonth()&&i==t.getFullYear(),a=this.hasDayValue?this.day:n?t.getDate():1;this.now=new Date(i,e,a).toISOString()}get current(){return typeof this.year=="number"&&typeof this.month=="number"&&typeof this.day=="number"?g(this.year,this.month,this.day):null}set current(t){y(t)&&(this.day=t.getDate(),this.month=t.getMonth(),this.year=t.getFullYear())}navigate=async t=>{if(!y(t))return;const e=this.current,i=t.toISOString(),n=e.toISOString();this.dispatch("navigate",{detail:{from:n,to:i}}),this.current=t,this.build(),await this.awaitCallback(this.onNavigated,{from:n,to:i}),this.dispatch("navigated",{detail:{from:n,to:i}})};step=async(t,e)=>{if(e===0)return;const i=this.current;switch(t){case"year":{i.setFullYear(i.getFullYear()+e);break}case"month":{i.setMonth(i.getMonth()+e);break}case"day":{i.setDate(i.getDate()+e);break}default:return}await this.navigate(i)};isDisabled=t=>{if(!y(t))return!1;if(this.disabledDates.length){const e=t.getTime();for(const i of this.disabledDates)if(e===new Date(i).getTime())return!0}if(this.disabledWeekdays.length){const e=t.getDay(),i=this.daysOfWeek,n=i.findIndex(a=>a.value===e);if(n>=0){const a=i[n];for(const r of this.disabledWeekdays)if(a.value==r||a.short===r||a.long===r)return!0}}if(this.disabledDays.length){const e=t.getDate();for(const i of this.disabledDays)if(e==i)return!0}if(this.disabledMonths.length){const e=t.getMonth(),i=this.monthsOfYear,n=i.findIndex(a=>a.value===e);if(n>=0){const a=i[n];for(const r of this.disabledMonths)if(a.value==r||a.short===r||a.long===r)return!0}}if(this.disabledYears.length){const e=t.getFullYear();for(const i of this.disabledYears)if(e==i)return!0}return!1};isWithinRange=t=>{if(!y(t))return!1;let e=!0;return this.since&&(e=e&&t>=this.since),this.till&&(e=e&&t<=this.till),e};enhance(){const t=this;Object.assign(this.controller,{get calendar(){return{get today(){return t.today},get current(){return t.current},get day(){return t.day},get month(){return t.month},get year(){return t.year},get since(){return t.since},get till(){return t.till},get firstDayOfWeek(){return t.firstDayOfWeek},get disabledDates(){return t.disabledDates},get disabledWeekdays(){return t.disabledWeekdays},get disabledDays(){return t.disabledDays},get disabledMonths(){return t.disabledMonths},get disabledYears(){return t.disabledYears},get daysOfWeek(){return t.daysOfWeek},get daysOfMonth(){return t.daysOfMonth},get monthsOfYear(){return t.monthsOfYear},navigate:async e=>await t.navigate(e),step:async(e,i)=>await t.step(e,i),isDisabled:e=>t.isDisabled(e),isWithinRange:e=>t.isWithinRange(e)}}})}}const rt=(s,t)=>new at(s,t),C={content:null,url:"",reload:"never",stale:3600,onLoad:"contentLoad",onLoading:"contentLoading",onLoaded:"contentLoaded"};class ot extends p{constructor(t,e={}){super(t,e);const i=Object.assign({},C,e),{content:n,url:a,reload:r,stale:l}=i;this.content=n,this.url=a,this.reload=typeof r=="string"?r:C.reload,this.stale=typeof l=="number"?l:C.stale;const{onLoad:h,onLoading:f,onLoaded:c}=i;this.onLoad=h,this.onLoading=f,this.onLoaded=c,this.enhance()}get reloadable(){switch(this.reload){case"never":return!1;case"always":return!0;default:{const t=g(this.loadedAt);return t&&new Date-t>this.stale*1e3}}}contentLoadable=({url:t})=>!!t;contentLoading=async({url:t})=>t?await this.remoteContentLoader(t):await this.contentLoader();contentLoader=async()=>"";remoteContentLoader=async t=>(await fetch(t)).text();load=async()=>{if(this.loadedAt&&!this.reloadable)return;const t=this.findCallback(this.onLoad),e=await this.awaitCallback(t||this.contentLoadable,{url:this.url});if(this.dispatch("load",{detail:{url:this.url}}),!e)return;const i=this.url?await this.remoteContentLoader(this.url):await this.contentLoader();this.dispatch("loading",{detail:{url:this.url}}),i&&(await this.awaitCallback(this.onLoaded,{url:this.url,content:i}),this.loadedAt=new Date().getTime(),this.dispatch("loaded",{detail:{url:this.url,content:i}}))};enhance(){const t=this;Object.assign(this.controller,{load:t.load.bind(t)})}}const lt=(s,t)=>new ot(s,t),ht={trigger:null,events:["click"],onDismissed:"dismissed"};class ct extends p{constructor(t,e={}){super(t,e);const{trigger:i,events:n,onDismissed:a}=Object.assign({},ht,e);this.onDismissed=a,this.trigger=i||this.element,this.events=n,this.enhance(),this.observe()}dismiss=async t=>{const{target:e}=t;e instanceof HTMLElement&&(this.element.contains(e)||this.visible&&(this.dispatch("dismiss"),await this.awaitCallback(this.onDismissed,{target:this.trigger}),this.dispatch("dismissed")))};observe(){this.events.forEach(t=>{window.addEventListener(t,this.dismiss,!0)})}unobserve(){this.events.forEach(t=>{window.removeEventListener(t,this.dismiss,!0)})}enhance(){const t=this,e=t.controller.disconnect.bind(t.controller);Object.assign(this.controller,{disconnect:()=>{t.unobserve(),e()}})}}const Y=(s,t)=>new ct(s,t),dt={anchor:null,events:["click"],placement:"bottom",alignment:"start",onFlipped:"flipped",ariaRole:null,respectMotion:!0};class ut extends p{constructor(t,e={}){super(t,e);const{anchor:i,events:n,placement:a,alignment:r,onFlipped:l,ariaRole:h,respectMotion:f}=Object.assign({},dt,e);this.anchor=i,this.events=n,this.placement=a,this.alignment=r,this.onFlipped=l,this.ariaRole=h,this.respectMotion=f,this.prefersReducedMotion=window.matchMedia("(prefers-reduced-motion: reduce)").matches,this.anchor&&this.element&&F({trigger:this.anchor,target:this.element,role:this.ariaRole}),this.enhance(),this.observe()}flip=async()=>{if(!this.visible)return;this.dispatch("flip"),window.getComputedStyle(this.element).position!="absolute"&&(this.element.style.position="absolute");const e=this.flippedRect(this.anchor.getBoundingClientRect(),this.element.getBoundingClientRect());this.element.style.transition=this.respectMotion&&this.prefersReducedMotion?"none":"";for(const[i,n]of Object.entries(e))this.element.style[i]=n;await this.awaitCallback(this.onFlipped,{target:this.element,placement:e}),this.dispatch("flipped",{detail:{placement:e}})};flippedRect(t,e){const i=this.quadrumRect(t,O()),n=[this.placement,V[this.placement]];let a={};for(;!Object.keys(a).length&&n.length>0;){const r=n.shift();if(!this.biggerRectThan(i[r],e))continue;const l=this.quadrumPlacement(t,r,e),h=this.quadrumAlignment(t,r,l);a.top=`${h.top+window.scrollY}px`,a.left=`${h.left+window.scrollX}px`}return Object.keys(a).length||(a.top="",a.left=""),a}quadrumRect(t,e){return{left:u({x:e.x,y:e.y,width:t.x-e.x,height:e.height}),right:u({x:t.x+t.width,y:e.y,width:e.width-(t.x+t.width),height:e.height}),top:u({x:e.x,y:e.y,width:e.width,height:t.y-e.y}),bottom:u({x:e.x,y:t.y+t.height,width:e.width,height:e.height-(t.y+t.height)})}}quadrumPlacement(t,e,i){switch(e){case"top":return u({x:i.x,y:t.y-i.height,width:i.width,height:i.height});case"bottom":return u({x:i.x,y:t.y+t.height,width:i.width,height:i.height});case"left":return u({x:t.x-i.width,y:i.y,width:i.width,height:i.height});case"right":return u({x:t.x+t.width,y:i.y,width:i.width,height:i.height});default:throw`Unable place at the quadrum, ${e}`}}quadrumAlignment(t,e,i){switch(e){case"top":case"bottom":{let n=t.x;return this.alignment==="center"?n=t.x+t.width/2-i.width/2:this.alignment==="end"&&(n=t.x+t.width-i.width),u({x:n,y:i.y,width:i.width,height:i.height})}case"left":case"right":{let n=t.y;return this.alignment==="center"?n=t.y+t.height/2-i.height/2:this.alignment==="end"&&(n=t.y+t.height-i.height),u({x:i.x,y:n,width:i.width,height:i.height})}default:throw`Unable align at the quadrum, ${e}`}}biggerRectThan(t,e){return t.height>=e.height&&t.width>=e.width}observe(){this.events.forEach(t=>{window.addEventListener(t,this.flip,!0)})}unobserve(){this.events.forEach(t=>{window.removeEventListener(t,this.flip,!0)})}enhance(){const t=this,e=t.controller.disconnect.bind(t.controller);Object.assign(this.controller,{disconnect:()=>{t.unobserve(),e()},flip:t.flip.bind(t)})}}const ft=(s,t)=>new ut(s,t),gt={events:["resize"],boundaries:["top","left","right"],onShifted:"shifted",respectMotion:!0};class mt extends p{constructor(t,e={}){super(t,e);const{onShifted:i,events:n,boundaries:a,respectMotion:r}=Object.assign({},gt,e);this.onShifted=i,this.events=n,this.boundaries=a,this.respectMotion=r,this.prefersReducedMotion=window.matchMedia("(prefers-reduced-motion: reduce)").matches,this.enhance(),this.observe()}shift=async()=>{if(!this.visible)return;this.dispatch("shift");const t=this.overflowRect(this.element.getBoundingClientRect(),this.elementTranslations(this.element)),e=t.left||t.right||0,i=t.top||t.bottom||0;this.element.style.transition=this.respectMotion&&this.prefersReducedMotion?"none":"",this.element.style.transform=`translate(${e}px, ${i}px)`,await this.awaitCallback(this.onShifted,t),this.dispatch("shifted",{detail:t})};overflowRect(t,e){const i={},n=O(),a=u({x:t.x-e.x,y:t.y-e.y,width:t.width,height:t.height});for(const r of this.boundaries){const l=this.directionDistance(a,r,n),h=V[r];l<0?a[h]+l>=n[h]&&!i[h]&&(i[r]=l):i[r]=""}return i}directionDistance(t,e,i){switch(e){case"top":case"left":return t[e]-i[e];case"bottom":case"right":return i[e]-t[e];default:throw`Invalid direction to calcuate distance, ${e}`}}elementTranslations(t){const e=window.getComputedStyle(t),i=e.transform||e.webkitTransform||e.mozTransform;if(i==="none"||typeof i>"u")return{x:0,y:0};const n=i.includes("3d")?"3d":"2d",a=i.match(/matrix.*\((.+)\)/)[1].split(", ");return n==="2d"?{x:Number(a[4]),y:Number(a[5])}:{x:0,y:0}}observe(){this.events.forEach(t=>{window.addEventListener(t,this.shift,!0)})}unobserve(){this.events.forEach(t=>{window.removeEventListener(t,this.shift,!0)})}enhance(){const t=this,e=t.controller.disconnect.bind(t.controller);Object.assign(this.controller,{disconnect:()=>{t.unobserve(),e()},shift:t.shift.bind(t)})}}const yt=(s,t)=>new mt(s,t),H={visibility:"visibility",onShown:"shown",onHidden:"hidden"};class pt extends p{constructor(t,e={}){const{visibility:i,onShown:n,onHidden:a,activator:r}=Object.assign({},H,e),l=typeof i=="string"?i:H.namespace,h=typeof e.visible=="string"?e.visible:"isVisible";(typeof e.visible!="boolean"||e.visible)&&(e.visible=`${l}.${h}`),super(t,e),this.visibility=l,this.visibilityResolver=h,this.onShown=n,this.onHidden=a,this.activator=r instanceof HTMLElement?r:null,this.enhance(),this.element instanceof HTMLElement&&this.activate(this.isVisible(this.element))}isVisible(t){if(!(t instanceof HTMLElement))return!1;const e=I.hiddenClass;return e?!t.classList.contains(e):!t.hasAttribute("hidden")}toggle(t,e){t instanceof HTMLElement&&(e?t.removeAttribute("hidden"):t.setAttribute("hidden",!0))}activate(t){this.activator&&this.activator.setAttribute("aria-expanded",t?"true":"false")}async show(){!(this.element instanceof HTMLElement)||this.isVisible(this.element)||(this.dispatch("show"),this.toggle(this.element,!0),this.activate(!0),await this.awaitCallback(this.onShown,{target:this.element}),this.dispatch("shown"))}async hide(){!(this.element instanceof HTMLElement)||!this.isVisible(this.element)||(this.dispatch("hide"),this.toggle(this.element,!1),this.activate(!1),await this.awaitCallback(this.onHidden,{target:this.element}),this.dispatch("hidden"))}enhance(){const t=this,e={show:t.show.bind(t),hide:t.hide.bind(t)};Object.defineProperty(e,"visible",{get(){return t.isVisible(t.element)}}),Object.defineProperty(e,this.visibilityResolver,{value:t.isVisible.bind(t)}),Object.defineProperty(this.controller,this.visibility,{get(){return e}})}}const j=(s,t)=>new pt(s,t);class bt extends d.Controller{static targets=["modal","overlay"];connect(){if(!this.hasModalTarget){console.error('ModalController requires a modal target. Add data-modal-target="modal" to your element.');return}this.isNativeDialog=this.modalTarget instanceof HTMLDialogElement,this.isNativeDialog?(this.modalTarget.addEventListener("cancel",this.close),this.modalTarget.addEventListener("click",this.handleBackdropClick)):(this.focusTrap=new x(this.modalTarget,{escapeDeactivates:!0}),Y(this,{element:this.modalTarget}))}dismissed=()=>{this.close()};disconnect(){this.isNativeDialog&&(this.modalTarget.removeEventListener("cancel",this.close),this.modalTarget.removeEventListener("click",this.handleBackdropClick))}open(t){if(t&&t.preventDefault(),!!this.hasModalTarget){if(this.isNativeDialog)this.previouslyFocused=document.activeElement,this.modalTarget.showModal();else{const e=this.hasOverlayTarget?this.overlayTarget:this.modalTarget;e.hidden=!1,document.body.style.overflow="hidden",this.focusTrap&&this.focusTrap.activate()}D("Modal opened")}}close(t){if(t&&t.preventDefault(),!!this.hasModalTarget){if(this.isNativeDialog)this.modalTarget.close(),this.previouslyFocused&&this.previouslyFocused.isConnected&&setTimeout(()=>{this.previouslyFocused.focus()},0);else{const e=this.hasOverlayTarget?this.overlayTarget:this.modalTarget;e.hidden=!0,document.body.style.overflow="",this.focusTrap&&this.focusTrap.deactivate()}D("Modal closed")}}handleBackdropClick=t=>{const e=this.modalTarget.getBoundingClientRect();(t.clientY<e.top||t.clientY>e.bottom||t.clientX<e.left||t.clientX>e.right)&&this.close()}}class wt extends d.Controller{static targets=["trigger"];connect(){Y(this,{trigger:this.hasTriggerTarget?this.triggerTarget:null})}}class vt extends d.Controller{static targets=["anchor","reference"];static values={placement:{type:String,default:"bottom"},alignment:{type:String,default:"start"},role:{type:String,default:"tooltip"}};connect(){if(!this.hasReferenceTarget){console.error('FlipperController requires a reference target. Add data-flipper-target="reference" to your element.');return}if(!this.hasAnchorTarget){console.error('FlipperController requires an anchor target. Add data-flipper-target="anchor" to your element.');return}ft(this,{element:this.referenceTarget,anchor:this.anchorTarget,placement:this.placementValue,alignment:this.alignmentValue,ariaRole:this.roleValue})}}class Tt extends d.Controller{static targets=["content","template","loader","activator"];static classes=["hidden"];static values={url:String,loadedAt:String,reload:{type:String,default:"never"},staleAfter:{type:Number,default:3600}};connect(){lt(this,{element:this.hasContentTarget?this.contentTarget:null,url:this.hasUrlValue?this.urlValue:null}),this.hasContentTarget&&j(this,{element:this.contentTarget,activator:this.hasActivatorTarget?this.activatorTarget:null}),this.hasLoaderTarget&&j(this,{element:this.loaderTarget,visibility:"contentLoaderVisibility"})}async show(){await this.visibility.show()}async hide(){await this.visibility.hide()}async shown(){await this.load()}contentLoad(){return this.hasContentTarget&&this.contentTarget.tagName.toLowerCase()==="turbo-frame"?(this.hasUrlValue&&this.contentTarget.setAttribute("src",this.urlValue),!1):!0}async contentLoading(){this.hasLoaderTarget&&await this.contentLoaderVisibility.show()}async contentLoaded({content:t}){this.hasContentTarget&&this.contentTarget.replaceChildren(this.getContentNode(t)),this.hasLoaderTarget&&await this.contentLoaderVisibility.hide()}getContentNode(t){if(typeof t=="string"){const e=document.createElement("template");return e.innerHTML=t,document.importNode(e.content,!0)}return document.importNode(t,!0)}contentLoader(){if(this.hasTemplateTarget)return this.templateTarget instanceof HTMLTemplateElement?this.templateTarget.content:this.templateTarget.innerHTML}}class Dt extends d.Controller{static targets=["daysOfWeek","daysOfMonth"];static classes=["dayOfWeek","dayOfMonth"];static values={locales:{type:Array,default:["default"]},weekdayFormat:{type:String,default:"short"},dayFormat:{type:String,default:"numeric"},daysOfOtherMonth:{type:Boolean,default:!1}};initialize(){rt(this)}connect(){this.draw()}navigated(){this.draw()}draw(){this.drawDaysOfWeek(),this.drawDaysOfMonth()}createDayElement(t,{selectable:e=!1,disabled:i=!1}={}){const n=document.createElement(e?"button":"div");return n.tabIndex=-1,t?n.textContent=t:n.setAttribute("aria-hidden","true"),i&&(n instanceof HTMLButtonElement?n.disabled=!0:n.setAttribute("aria-disabled","true")),n}drawDaysOfWeek(){if(!this.hasDaysOfWeekTarget)return;const t=new Intl.DateTimeFormat(this.localesValue,{weekday:this.weekdayFormatValue}),e=[];for(const n of this.calendar.daysOfWeek){const a=this.createDayElement(t.format(n.date));a.setAttribute("role","columnheader"),a.title=n.long,this.hasDayOfWeekClass&&a.classList.add(...this.dayOfWeekClasses),e.push(a)}const i=document.createElement("div");i.setAttribute("role","row"),i.replaceChildren(...e),this.daysOfWeekTarget.replaceChildren(i)}drawDaysOfMonth(){if(!this.hasDaysOfMonthTarget)return;const t=this.calendar.today,e=new Date(t.getFullYear(),t.getMonth(),t.getDate()).getTime(),i=[];for(const a of this.calendar.daysOfMonth){const r=!a.current||this.calendar.isDisabled(a.date)||!this.calendar.isWithinRange(a.date),l=a.current||this.daysOfOtherMonthValue?a.value:"",h=this.createDayElement(l,{selectable:a.current,disabled:r});e===a.date.getTime()&&h.setAttribute("aria-current","date"),this.hasDayOfMonthClass&&h.classList.add(...this.dayOfMonthClasses);const f=document.createElement("time");f.dateTime=a.iso,h.appendChild(f),i.push(h)}const n=[];for(let a=0;a<i.length;a+=7){const r=document.createElement("div");r.setAttribute("role","row");for(const l of i.slice(a,a+7))l.setAttribute("role","gridcell"),r.appendChild(l);n.push(r)}this.daysOfMonthTarget.replaceChildren(...n)}}class Ot extends d.Controller{select(t){if(!(t.target instanceof HTMLElement))return;t.preventDefault();const e=t.target instanceof HTMLTimeElement?t.target.parentElement:t.target;if(e.disabled||e.getAttribute("aria-disabled")==="true")return;this.dispatch("select",{target:e});const i=t.target instanceof HTMLTimeElement?t.target:t.target.querySelector("time");if(!i)return console.error(`unable to locate time element within ${e}`);const n=g(i.dateTime);if(!n)return console.error(`unable to parse ${i.dateTime} found within the time element`);this.dispatch("selected",{target:e,detail:{epoch:n.getTime(),iso:n.toISOString()}})}}class Ct extends d.Controller{static targets=["previous","next","day","month","year","input","display"];static outlets=["calendar-month"];static values={locales:{type:Array,default:["default"]},dayFormat:{type:String,default:"numeric"},monthFormat:{type:String,default:"long"},yearFormat:{type:String,default:"numeric"}};initialize(){this.previous=this.previous.bind(this),this.next=this.next.bind(this)}async calendarMonthOutletConnected(){if(this.hasInputTarget&&this.inputTarget.value){const t=g(this.inputTarget.value);t&&await this.calendarMonthOutlet.calendar.navigate(t)}this.draw()}selected(t){this.hasInputTarget&&(this.inputTarget.value=t.detail.iso),this.hasDisplayTarget&&(this.displayTarget.value=this.formatDate(new Date(t.detail.iso)))}formatDate(t){return new Intl.DateTimeFormat(this.localesValue,{day:this.dayFormatValue,month:this.monthFormatValue,year:this.yearFormatValue}).format(t)}previousTargetConnected(t){t.addEventListener("click",this.previous)}previousTargetDisconnected(t){t.removeEventListener("click",this.previous)}async previous(){await this.calendarMonthOutlet.calendar.step("month",-1),this.draw()}nextTargetConnected(t){t.addEventListener("click",this.next)}nextTargetDisconnected(t){t.removeEventListener("click",this.next)}async next(){await this.calendarMonthOutlet.calendar.step("month",1),this.draw()}draw(){this.drawDay(),this.drawMonth(),this.drawYear()}drawDay(){if(!this.hasDayTarget||!this.hasCalendarMonthOutlet)return;const{year:t,month:e,day:i}=this.calendarMonthOutlet.calendar,n=new Intl.DateTimeFormat(this.localesValue,{day:this.dayFormatValue});this.dayTarget.textContent=n.format(new Date(t,e,i))}drawMonth(){if(!this.hasMonthTarget||!this.hasCalendarMonthOutlet)return;const{year:t,month:e}=this.calendarMonthOutlet.calendar,i=new Intl.DateTimeFormat(this.localesValue,{month:this.monthFormatValue});this.monthTarget.textContent=i.format(new Date(t,e))}drawYear(){if(!this.hasYearTarget||!this.hasCalendarMonthOutlet)return;const{year:t}=this.calendarMonthOutlet.calendar,e=new Intl.DateTimeFormat(this.localesValue,{year:this.yearFormatValue});this.yearTarget.textContent=e.format(new Date(t,0))}}class Mt extends d.Controller{static targets=["content"];connect(){yt(this,{element:this.hasContentTarget?this.contentTarget:null})}}class kt extends d.Controller{static targets=["input"];toggle(){this.inputTarget.type=this.inputTarget.type==="password"?"text":"password"}}class xt extends d.Controller{connect(){this.resize(),this.element.addEventListener("input",this.resize.bind(this))}disconnect(){this.element.removeEventListener("input",this.resize.bind(this))}resize(){this.element.style.height="auto",this.element.style.height=`${this.element.scrollHeight}px`}}o.ARIA_HASPOPUP_VALUES=A,o.AutoResizeController=xt,o.CalendarMonthController=Dt,o.CalendarMonthObserverController=Ot,o.DatepickerController=Ct,o.DismisserController=wt,o.FOCUSABLE_SELECTOR=M,o.FlipperController=vt,o.FocusRestoration=q,o.FocusTrap=x,o.ModalController=bt,o.PannerController=Mt,o.PasswordRevealController=kt,o.PopoverController=Tt,o.RovingTabIndex=X,o.announce=D,o.connectTriggerToTarget=F,o.disconnectTriggerFromTarget=it,o.ensureId=J,o.focusFirst=k,o.generateId=E,o.getFocusableElements=T,o.isActivationKey=K,o.isArrowKey=U,o.isKey=B,o.isVisible=w,o.preventDefault=z,o.setAriaState=b,o.setChecked=tt,o.setDisabled=et,o.setExpanded=Q,o.setPressed=Z,Object.defineProperty(o,Symbol.toStringTag,{value:"Module"})}));
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stimulus-plumbers/controllers",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Stimulus controllers following WCAG standards",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.esm.js",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"dev": "vite",
|
|
14
|
+
"build": "vite build",
|
|
15
|
+
"preview": "vite preview",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"test:ui": "vitest --ui",
|
|
18
|
+
"test:coverage": "vitest --coverage",
|
|
19
|
+
"lint": "eslint src",
|
|
20
|
+
"format:write": "prettier --write \"src/**/*.js\"",
|
|
21
|
+
"format:check": "prettier --check \"src/**/*.js\""
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"accessibility",
|
|
25
|
+
"a11y",
|
|
26
|
+
"stimulus",
|
|
27
|
+
"hotwire",
|
|
28
|
+
"aria"
|
|
29
|
+
],
|
|
30
|
+
"author": "Ryan Chang",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/ryanchang/stimulus-plumbers.git",
|
|
35
|
+
"directory": "stimulus"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@hotwired/stimulus": "^3.0.0 || ^2.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@eslint/js": "^10.0.1",
|
|
42
|
+
"@hotwired/stimulus": "^3.2.2",
|
|
43
|
+
"@testing-library/dom": "^10.4.0",
|
|
44
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
45
|
+
"@vitest/ui": "^4.1.4",
|
|
46
|
+
"axe-core": "^4.10.2",
|
|
47
|
+
"eslint": "^10.2.0",
|
|
48
|
+
"globals": "^17.4.0",
|
|
49
|
+
"jsdom": "^29.0.0",
|
|
50
|
+
"prettier": "^3.3.3",
|
|
51
|
+
"vite": "^8.0.4",
|
|
52
|
+
"vitest": "^4.1.4"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18.0.0"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/aria.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get or create a live region for screen reader announcements
|
|
3
|
+
*/
|
|
4
|
+
const getLiveRegion = (politeness, atomic, relevant) => {
|
|
5
|
+
let region = document.querySelector(`[data-live-region="${politeness}"]`);
|
|
6
|
+
|
|
7
|
+
if (!region) {
|
|
8
|
+
region = document.createElement('div');
|
|
9
|
+
region.className = 'sr-only';
|
|
10
|
+
region.dataset.liveRegion = politeness;
|
|
11
|
+
region.setAttribute('aria-live', politeness);
|
|
12
|
+
region.setAttribute('aria-atomic', atomic.toString());
|
|
13
|
+
region.setAttribute('aria-relevant', relevant);
|
|
14
|
+
document.body.appendChild(region);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return region;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Announce a message to screen readers using an aria-live region
|
|
22
|
+
*/
|
|
23
|
+
export function announce(message, options = {}) {
|
|
24
|
+
const { politeness = 'polite', atomic = true, relevant = 'additions text' } = options;
|
|
25
|
+
const region = getLiveRegion(politeness, atomic, relevant);
|
|
26
|
+
|
|
27
|
+
region.textContent = '';
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
region.textContent = message;
|
|
30
|
+
}, 100);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate a unique ID for ARIA relationships
|
|
35
|
+
*/
|
|
36
|
+
export const generateId = (prefix = 'a11y') => `${prefix}-${Math.random().toString(36).substr(2, 9)}`;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Ensure element has an ID, generate one if needed
|
|
40
|
+
*/
|
|
41
|
+
export const ensureId = (element, prefix = 'element') => element.id || (element.id = generateId(prefix));
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generic function to set ARIA state attributes
|
|
45
|
+
*/
|
|
46
|
+
export const setAriaState = (element, attribute, value) => {
|
|
47
|
+
element.setAttribute(attribute, value.toString());
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Update aria-expanded state
|
|
52
|
+
*/
|
|
53
|
+
export const setExpanded = (element, expanded) => setAriaState(element, 'aria-expanded', expanded);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Update aria-pressed state
|
|
57
|
+
*/
|
|
58
|
+
export const setPressed = (element, pressed) => setAriaState(element, 'aria-pressed', pressed);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Update aria-checked state
|
|
62
|
+
*/
|
|
63
|
+
export const setChecked = (element, checked) => setAriaState(element, 'aria-checked', checked);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Update aria-disabled state and manage tabindex
|
|
67
|
+
*/
|
|
68
|
+
export function setDisabled(element, disabled) {
|
|
69
|
+
setAriaState(element, 'aria-disabled', disabled);
|
|
70
|
+
disabled ? element.setAttribute('tabindex', '-1') : element.removeAttribute('tabindex');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Maps ARIA roles to their appropriate aria-haspopup values
|
|
75
|
+
*/
|
|
76
|
+
export const ARIA_HASPOPUP_VALUES = {
|
|
77
|
+
menu: 'menu',
|
|
78
|
+
listbox: 'listbox',
|
|
79
|
+
tree: 'tree',
|
|
80
|
+
grid: 'grid',
|
|
81
|
+
dialog: 'dialog',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Apply attributes to element if they should be set
|
|
86
|
+
*/
|
|
87
|
+
const applyAttributes = (element, attributes, result) => {
|
|
88
|
+
Object.entries(attributes).forEach(([attr, value]) => {
|
|
89
|
+
element.setAttribute(attr, value);
|
|
90
|
+
result[attr] = value;
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if attribute should be set based on override and existing value
|
|
96
|
+
*/
|
|
97
|
+
const shouldSet = (element, attribute, override) => override || !element.hasAttribute(attribute);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Connects a trigger element to a target element with ARIA relationships
|
|
101
|
+
* @param {Object} options - Configuration options
|
|
102
|
+
* @param {HTMLElement} options.trigger - Trigger/anchor element (e.g., button)
|
|
103
|
+
* @param {HTMLElement} options.target - Target/reference element (e.g., menu, dialog)
|
|
104
|
+
* @param {string} [options.role] - ARIA role for the target element
|
|
105
|
+
* @param {boolean} [options.override=false] - Override existing attributes
|
|
106
|
+
* @returns {Object} Object containing set attributes with trigger and target subobjects
|
|
107
|
+
*/
|
|
108
|
+
export function connectTriggerToTarget({ trigger, target, role = null, override = false }) {
|
|
109
|
+
const result = { trigger: {}, target: {} };
|
|
110
|
+
|
|
111
|
+
if (!trigger || !target) return result;
|
|
112
|
+
|
|
113
|
+
const triggerAttrs = {};
|
|
114
|
+
const targetAttrs = {};
|
|
115
|
+
|
|
116
|
+
// Set target role
|
|
117
|
+
if (role && shouldSet(target, 'role', override)) {
|
|
118
|
+
targetAttrs.role = role;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Set trigger attributes if target has an ID
|
|
122
|
+
if (target.id) {
|
|
123
|
+
if (shouldSet(trigger, 'aria-controls', override)) {
|
|
124
|
+
triggerAttrs['aria-controls'] = target.id;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (role === 'tooltip' && shouldSet(trigger, 'aria-describedby', override)) {
|
|
128
|
+
triggerAttrs['aria-describedby'] = target.id;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Set aria-haspopup based on role
|
|
133
|
+
if (role && shouldSet(trigger, 'aria-haspopup', override)) {
|
|
134
|
+
const haspopup = ARIA_HASPOPUP_VALUES[role] || 'true';
|
|
135
|
+
if (haspopup) triggerAttrs['aria-haspopup'] = haspopup;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
applyAttributes(target, targetAttrs, result.target);
|
|
139
|
+
applyAttributes(trigger, triggerAttrs, result.trigger);
|
|
140
|
+
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Remove attributes from element if they exist
|
|
146
|
+
*/
|
|
147
|
+
const removeAttributes = (element, attributes) => {
|
|
148
|
+
attributes.forEach((attr) => {
|
|
149
|
+
if (element.hasAttribute(attr)) {
|
|
150
|
+
element.removeAttribute(attr);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Disconnects a trigger element from a target element by removing ARIA relationships
|
|
157
|
+
* @param {Object} options - Configuration options
|
|
158
|
+
* @param {HTMLElement} options.trigger - Trigger element
|
|
159
|
+
* @param {HTMLElement} options.target - Target element
|
|
160
|
+
* @param {string[]} [options.attributes] - Specific attributes to remove (default: all)
|
|
161
|
+
*/
|
|
162
|
+
export function disconnectTriggerFromTarget({ trigger, target, attributes = null }) {
|
|
163
|
+
if (!trigger || !target) return;
|
|
164
|
+
|
|
165
|
+
const defaultAttrs = ['aria-controls', 'aria-haspopup', 'aria-describedby'];
|
|
166
|
+
const attrsToRemove = attributes || defaultAttrs;
|
|
167
|
+
|
|
168
|
+
removeAttributes(trigger, attrsToRemove);
|
|
169
|
+
|
|
170
|
+
if (!attributes || attributes.includes('role')) {
|
|
171
|
+
removeAttributes(target, ['role']);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
connect() {
|
|
5
|
+
this.resize();
|
|
6
|
+
this.element.addEventListener('input', this.resize.bind(this));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
disconnect() {
|
|
10
|
+
this.element.removeEventListener('input', this.resize.bind(this));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
resize() {
|
|
14
|
+
this.element.style.height = 'auto';
|
|
15
|
+
this.element.style.height = `${this.element.scrollHeight}px`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
import { initCalendar } from '../plumbers';
|
|
3
|
+
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static targets = ['daysOfWeek', 'daysOfMonth'];
|
|
6
|
+
static classes = ['dayOfWeek', 'dayOfMonth'];
|
|
7
|
+
static values = {
|
|
8
|
+
locales: { type: Array, default: ['default'] },
|
|
9
|
+
weekdayFormat: { type: String, default: 'short' },
|
|
10
|
+
dayFormat: { type: String, default: 'numeric' },
|
|
11
|
+
daysOfOtherMonth: { type: Boolean, default: false },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
initialize() {
|
|
15
|
+
initCalendar(this);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
connect() {
|
|
19
|
+
this.draw();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
navigated() {
|
|
23
|
+
this.draw();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
draw() {
|
|
27
|
+
this.drawDaysOfWeek();
|
|
28
|
+
this.drawDaysOfMonth();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
createDayElement(day, { selectable = false, disabled = false } = {}) {
|
|
32
|
+
const element = document.createElement(selectable ? 'button' : 'div');
|
|
33
|
+
element.tabIndex = -1;
|
|
34
|
+
if (day) element.textContent = day;
|
|
35
|
+
else element.setAttribute('aria-hidden', 'true');
|
|
36
|
+
if (disabled) {
|
|
37
|
+
if (element instanceof HTMLButtonElement) element.disabled = true;
|
|
38
|
+
else element.setAttribute('aria-disabled', 'true');
|
|
39
|
+
}
|
|
40
|
+
return element;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
drawDaysOfWeek() {
|
|
44
|
+
if (!this.hasDaysOfWeekTarget) return;
|
|
45
|
+
|
|
46
|
+
const formatter = new Intl.DateTimeFormat(this.localesValue, {
|
|
47
|
+
weekday: this.weekdayFormatValue,
|
|
48
|
+
});
|
|
49
|
+
const daysOfWeek = [];
|
|
50
|
+
for (const date of this.calendar.daysOfWeek) {
|
|
51
|
+
const dayElement = this.createDayElement(formatter.format(date.date));
|
|
52
|
+
dayElement.setAttribute('role', 'columnheader');
|
|
53
|
+
dayElement.title = date.long;
|
|
54
|
+
if (this.hasDayOfWeekClass) dayElement.classList.add(...this.dayOfWeekClasses);
|
|
55
|
+
daysOfWeek.push(dayElement);
|
|
56
|
+
}
|
|
57
|
+
const row = document.createElement('div');
|
|
58
|
+
row.setAttribute('role', 'row');
|
|
59
|
+
row.replaceChildren(...daysOfWeek);
|
|
60
|
+
this.daysOfWeekTarget.replaceChildren(row);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
drawDaysOfMonth() {
|
|
64
|
+
if (!this.hasDaysOfMonthTarget) return;
|
|
65
|
+
|
|
66
|
+
const t = this.calendar.today;
|
|
67
|
+
const today = new Date(t.getFullYear(), t.getMonth(), t.getDate()).getTime();
|
|
68
|
+
const daysOfMonth = [];
|
|
69
|
+
for (const date of this.calendar.daysOfMonth) {
|
|
70
|
+
const dayDisabled =
|
|
71
|
+
!date.current || this.calendar.isDisabled(date.date) || !this.calendar.isWithinRange(date.date);
|
|
72
|
+
const dayText = date.current || this.daysOfOtherMonthValue ? date.value : '';
|
|
73
|
+
const dayElement = this.createDayElement(dayText, {
|
|
74
|
+
selectable: date.current,
|
|
75
|
+
disabled: dayDisabled,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (today === date.date.getTime()) dayElement.setAttribute('aria-current', 'date');
|
|
79
|
+
if (this.hasDayOfMonthClass) dayElement.classList.add(...this.dayOfMonthClasses);
|
|
80
|
+
|
|
81
|
+
const time = document.createElement('time');
|
|
82
|
+
time.dateTime = date.iso;
|
|
83
|
+
dayElement.appendChild(time);
|
|
84
|
+
|
|
85
|
+
daysOfMonth.push(dayElement);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const rows = [];
|
|
89
|
+
for (let i = 0; i < daysOfMonth.length; i += 7) {
|
|
90
|
+
const row = document.createElement('div');
|
|
91
|
+
row.setAttribute('role', 'row');
|
|
92
|
+
for (const day of daysOfMonth.slice(i, i + 7)) {
|
|
93
|
+
day.setAttribute('role', 'gridcell');
|
|
94
|
+
row.appendChild(day);
|
|
95
|
+
}
|
|
96
|
+
rows.push(row);
|
|
97
|
+
}
|
|
98
|
+
this.daysOfMonthTarget.replaceChildren(...rows);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
import { tryParseDate } from '../plumbers/plumber/support';
|
|
3
|
+
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
select(event) {
|
|
6
|
+
if (!(event.target instanceof HTMLElement)) return;
|
|
7
|
+
|
|
8
|
+
event.preventDefault();
|
|
9
|
+
const input = event.target instanceof HTMLTimeElement ? event.target.parentElement : event.target;
|
|
10
|
+
if (input.disabled || input.getAttribute('aria-disabled') === 'true') return;
|
|
11
|
+
|
|
12
|
+
this.dispatch('select', { target: input });
|
|
13
|
+
const time = event.target instanceof HTMLTimeElement ? event.target : event.target.querySelector('time');
|
|
14
|
+
if (!time) return console.error(`unable to locate time element within ${input}`);
|
|
15
|
+
|
|
16
|
+
const date = tryParseDate(time.dateTime);
|
|
17
|
+
if (!date) return console.error(`unable to parse ${time.dateTime} found within the time element`);
|
|
18
|
+
|
|
19
|
+
this.dispatch('selected', {
|
|
20
|
+
target: input,
|
|
21
|
+
detail: { epoch: date.getTime(), iso: date.toISOString() },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
import { tryParseDate } from '../plumbers/plumber/support';
|
|
3
|
+
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static targets = ['previous', 'next', 'day', 'month', 'year', 'input', 'display'];
|
|
6
|
+
static outlets = ['calendar-month'];
|
|
7
|
+
static values = {
|
|
8
|
+
locales: { type: Array, default: ['default'] },
|
|
9
|
+
dayFormat: { type: String, default: 'numeric' },
|
|
10
|
+
monthFormat: { type: String, default: 'long' },
|
|
11
|
+
yearFormat: { type: String, default: 'numeric' },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
initialize() {
|
|
15
|
+
this.previous = this.previous.bind(this);
|
|
16
|
+
this.next = this.next.bind(this);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async calendarMonthOutletConnected() {
|
|
20
|
+
if (this.hasInputTarget && this.inputTarget.value) {
|
|
21
|
+
const date = tryParseDate(this.inputTarget.value);
|
|
22
|
+
if (date) {
|
|
23
|
+
await this.calendarMonthOutlet.calendar.navigate(date);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
this.draw();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
selected(event) {
|
|
30
|
+
if (this.hasInputTarget) {
|
|
31
|
+
this.inputTarget.value = event.detail.iso;
|
|
32
|
+
}
|
|
33
|
+
if (this.hasDisplayTarget) {
|
|
34
|
+
this.displayTarget.value = this.formatDate(new Date(event.detail.iso));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
formatDate(date) {
|
|
39
|
+
return new Intl.DateTimeFormat(this.localesValue, {
|
|
40
|
+
day: this.dayFormatValue,
|
|
41
|
+
month: this.monthFormatValue,
|
|
42
|
+
year: this.yearFormatValue,
|
|
43
|
+
}).format(date);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
previousTargetConnected(target) {
|
|
47
|
+
target.addEventListener('click', this.previous);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
previousTargetDisconnected(target) {
|
|
51
|
+
target.removeEventListener('click', this.previous);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async previous() {
|
|
55
|
+
await this.calendarMonthOutlet.calendar.step('month', -1);
|
|
56
|
+
this.draw();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
nextTargetConnected(target) {
|
|
60
|
+
target.addEventListener('click', this.next);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
nextTargetDisconnected(target) {
|
|
64
|
+
target.removeEventListener('click', this.next);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async next() {
|
|
68
|
+
await this.calendarMonthOutlet.calendar.step('month', 1);
|
|
69
|
+
this.draw();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
draw() {
|
|
73
|
+
this.drawDay();
|
|
74
|
+
this.drawMonth();
|
|
75
|
+
this.drawYear();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
drawDay() {
|
|
79
|
+
if (!this.hasDayTarget || !this.hasCalendarMonthOutlet) return;
|
|
80
|
+
|
|
81
|
+
const { year, month, day } = this.calendarMonthOutlet.calendar;
|
|
82
|
+
const formatter = new Intl.DateTimeFormat(this.localesValue, { day: this.dayFormatValue });
|
|
83
|
+
this.dayTarget.textContent = formatter.format(new Date(year, month, day));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
drawMonth() {
|
|
87
|
+
if (!this.hasMonthTarget || !this.hasCalendarMonthOutlet) return;
|
|
88
|
+
|
|
89
|
+
const { year, month } = this.calendarMonthOutlet.calendar;
|
|
90
|
+
const formatter = new Intl.DateTimeFormat(this.localesValue, { month: this.monthFormatValue });
|
|
91
|
+
this.monthTarget.textContent = formatter.format(new Date(year, month));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
drawYear() {
|
|
95
|
+
if (!this.hasYearTarget || !this.hasCalendarMonthOutlet) return;
|
|
96
|
+
|
|
97
|
+
const { year } = this.calendarMonthOutlet.calendar;
|
|
98
|
+
const formatter = new Intl.DateTimeFormat(this.localesValue, { year: this.yearFormatValue });
|
|
99
|
+
this.yearTarget.textContent = formatter.format(new Date(year, 0));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
import { attachDismisser } from '../plumbers';
|
|
3
|
+
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static targets = ['trigger'];
|
|
6
|
+
|
|
7
|
+
connect() {
|
|
8
|
+
attachDismisser(this, { trigger: this.hasTriggerTarget ? this.triggerTarget : null });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
import { attachFlipper } from '../plumbers';
|
|
3
|
+
|
|
4
|
+
export default class FlipperController extends Controller {
|
|
5
|
+
static targets = ['anchor', 'reference'];
|
|
6
|
+
static values = {
|
|
7
|
+
placement: { type: String, default: 'bottom' },
|
|
8
|
+
alignment: { type: String, default: 'start' },
|
|
9
|
+
role: { type: String, default: 'tooltip' },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
connect() {
|
|
13
|
+
if (!this.hasReferenceTarget) {
|
|
14
|
+
console.error(
|
|
15
|
+
'FlipperController requires a reference target. Add data-flipper-target="reference" to your element.'
|
|
16
|
+
);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!this.hasAnchorTarget) {
|
|
21
|
+
console.error('FlipperController requires an anchor target. Add data-flipper-target="anchor" to your element.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
attachFlipper(this, {
|
|
26
|
+
element: this.referenceTarget,
|
|
27
|
+
anchor: this.anchorTarget,
|
|
28
|
+
placement: this.placementValue,
|
|
29
|
+
alignment: this.alignmentValue,
|
|
30
|
+
ariaRole: this.roleValue,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
import { attachDismisser, attachVisibility } from '../plumbers';
|
|
3
|
+
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static targets = ['dropdown', 'trigger', 'input'];
|
|
6
|
+
static values = {
|
|
7
|
+
inputPath: { type: String, default: 'detail.value' },
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
initialize() {
|
|
11
|
+
this.update = this.update.bind(this);
|
|
12
|
+
this.expand = this.expand.bind(this);
|
|
13
|
+
this.collapse = this.collapse.bind(this);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
connect() {
|
|
17
|
+
attachDismisser(this, { events: ['click', 'focus'] });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
triggerTargetConnected(target) {
|
|
21
|
+
target.addEventListener('click', this.expand);
|
|
22
|
+
target.addEventListener('focus', this.expand);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
triggerTargetDisconnected(target) {
|
|
26
|
+
target.removeEventListener('click', this.expand);
|
|
27
|
+
target.removeEventListener('focus', this.expand);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
dropdownTargetConnected(target) {
|
|
31
|
+
attachVisibility(this, { element: target, visibility: 'dropdownVisibility' });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async update(event) {
|
|
35
|
+
const value = this.inputPathValue.split('.').reduce((acc, key) => acc && acc[key], event);
|
|
36
|
+
if (this.hasInputTarget) this.updateField(this.inputTarget, value);
|
|
37
|
+
await this.collapse();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
updateField(element, value) {
|
|
41
|
+
const attr = this.fieldAttribute(element);
|
|
42
|
+
const from = element[attr];
|
|
43
|
+
|
|
44
|
+
this.dispatch('update', { target: element, from, to: value });
|
|
45
|
+
element[attr] = value;
|
|
46
|
+
this.dispatch('updated', { target: element, from, to: value });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fieldAttribute(element) {
|
|
50
|
+
switch (element.constructor) {
|
|
51
|
+
case HTMLInputElement:
|
|
52
|
+
case HTMLTextAreaElement:
|
|
53
|
+
case HTMLButtonElement: {
|
|
54
|
+
return 'value';
|
|
55
|
+
}
|
|
56
|
+
default: {
|
|
57
|
+
return 'textContent';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async expand(_event) {
|
|
63
|
+
if (!this.hasDropdownTarget || this.dropdownVisibility.visible) return;
|
|
64
|
+
|
|
65
|
+
await this.dropdownVisibility.show();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async collapse(_event) {
|
|
69
|
+
if (!this.hasDropdownTarget || !this.dropdownVisibility.visible) return;
|
|
70
|
+
|
|
71
|
+
await this.dropdownVisibility.hide();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async dismissed() {
|
|
75
|
+
await this.collapse();
|
|
76
|
+
}
|
|
77
|
+
}
|