katalyst-kpop 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +113 -0
- data/app/assets/builds/katalyst/kpop.css +117 -0
- data/app/assets/builds/katalyst/kpop.min.js +1 -0
- data/app/assets/config/kpop.js +1 -0
- data/app/assets/javascripts/controllers/kpop_controller.js +69 -0
- data/app/assets/javascripts/controllers/scrim_controller.js +162 -0
- data/app/assets/javascripts/katalyst/kpop.js +9 -0
- data/app/assets/stylesheets/katalyst/kpop/_index.scss +2 -0
- data/app/assets/stylesheets/katalyst/kpop/_kpop.scss +133 -0
- data/app/assets/stylesheets/katalyst/kpop/_scrim.scss +16 -0
- data/app/assets/stylesheets/katalyst/kpop.scss +1 -0
- data/app/helpers/kpop/modal.rb +98 -0
- data/app/helpers/kpop_helper.rb +39 -0
- data/app/helpers/scrim_helper.rb +12 -0
- data/app/views/layouts/kpop.html.erb +4 -0
- data/config/importmap.rb +6 -0
- data/config/routes.rb +2 -0
- data/lib/katalyst/kpop/engine.rb +36 -0
- data/lib/katalyst/kpop/version.rb +7 -0
- data/lib/katalyst/kpop.rb +9 -0
- data/lib/tasks/yarn.rake +23 -0
- metadata +65 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bfe556211c8cb27976e75cf940c7ae065fc6fbfb31f8a9fb2c78ec6e3d36db4d
|
|
4
|
+
data.tar.gz: 7014e09c2697ff7e0d0093f67068b1583a21c8bdb165222df6b990e63869eac0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7affe4e861d299c11391520a71adf5d7a9fa4e1af699d7bd9938b0c784784c531992fd68d7d8ea14c58a974b2f6a52ff6e021bcab5811874730e006b1d7d31e9
|
|
7
|
+
data.tar.gz: d087f450b624e8dc6e4c2426672eb697e52f6e5fe3bbac402f2b71458d0c35ce8f956a60b6c6446f9a6bce4100fbb33865dd4af8550ce12fe0342540a59add1d
|
data/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# kpop
|
|
2
|
+
|
|
3
|
+
Modals driven by `@hotwire/turbo` frame navigation.
|
|
4
|
+
|
|
5
|
+
kpop requires `@hotwire/turbo` and `@hotwire/stimulus` to be installed and configured correctly to be used.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Install gem
|
|
10
|
+
```bash
|
|
11
|
+
# Gemfile
|
|
12
|
+
|
|
13
|
+
$ bundle add "katalyst-kpop"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
kpop supports installation of javascript dependencies with either import maps or yarn.
|
|
17
|
+
|
|
18
|
+
### Stimulus controllers
|
|
19
|
+
|
|
20
|
+
If you are using asset pipeline and import maps then the stimulus controllers
|
|
21
|
+
for modals and scrim will be automatically available without configuration.
|
|
22
|
+
|
|
23
|
+
### Stylesheets
|
|
24
|
+
|
|
25
|
+
Import stylesheets through using SASS using asset pipeline:
|
|
26
|
+
|
|
27
|
+
```scss
|
|
28
|
+
// app/assets/stylesheets/application.scss
|
|
29
|
+
|
|
30
|
+
@use "katalyst/kpop";
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
You can also load a precompiled version from the gem directly:
|
|
34
|
+
|
|
35
|
+
```erb
|
|
36
|
+
<%# app/views/layouts/application.html.erb #>
|
|
37
|
+
|
|
38
|
+
<%= stylesheet_link_tag "katalyst/kpop" %>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Yarn
|
|
42
|
+
|
|
43
|
+
If you are not using import maps, you can add the yarn package to your project:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
$ yarn add "@katalyst-interactive/kpop"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Import kpop styles
|
|
50
|
+
```css
|
|
51
|
+
/* application.scss */
|
|
52
|
+
|
|
53
|
+
@import "~@katalyst-interactive/kpop";
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Import kpop stimulus controllers
|
|
57
|
+
```js
|
|
58
|
+
/* application.js */
|
|
59
|
+
import kpop from "@katalyst-interactive/kpop"
|
|
60
|
+
application.load(kpop)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
kpop provides helpers to add a basic scrim and modal target frame. These should be placed inside the body:
|
|
66
|
+
```html
|
|
67
|
+
<body>
|
|
68
|
+
<%= scrim_tag %>
|
|
69
|
+
<%= kpop_frame_tag do %>
|
|
70
|
+
<%= yield :kpop %>
|
|
71
|
+
<% end %>
|
|
72
|
+
</body>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Show a modal
|
|
76
|
+
|
|
77
|
+
To show a modal you need to add content to the kpop turbo frame. You can do this in several ways:
|
|
78
|
+
1. Use `content_for :kpop` in an HTML response to inject content into the kpop frame (see `yield :kpop` above)
|
|
79
|
+
2. Use `layout "kpop"` in your controller to wrap your turbo response in a kpop frame
|
|
80
|
+
|
|
81
|
+
You can generate a link that will cause a modal to show using the `kpop_link_to` helper.
|
|
82
|
+
|
|
83
|
+
`kpop_link_to`'s are similar to a `link_to` in rails, but it will navigate to the given URL within the modal turbo
|
|
84
|
+
frame. The targeted action will need to generate content in a `kpop_frame_tag`, e.g. using `layout "kpop"`.
|
|
85
|
+
|
|
86
|
+
```html
|
|
87
|
+
<!-- app/views/homepage/index.html.erb -->
|
|
88
|
+
<%= modal_link_to "click to open modal", modal_path("example") %>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```html
|
|
92
|
+
<!-- app/views/modals/show.html.erb -->
|
|
93
|
+
<%= render_kpop(title: "Modal title") do %>
|
|
94
|
+
Modal content
|
|
95
|
+
<% end %>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Note that, because kpop modals render in a turbo frame, if you want to navigate the parent frame you will need to use
|
|
99
|
+
`target: "_top"` on your links and forms.
|
|
100
|
+
|
|
101
|
+
## Development
|
|
102
|
+
|
|
103
|
+
Releases need to be distributed to rubygems.org and npmjs.org. To do this, you need to have accounts with both providers
|
|
104
|
+
and be added as a collaborator to the kpop gem and npm packages.
|
|
105
|
+
|
|
106
|
+
1. Update the version in `package.json` and `lib/katalyst/kpop/version.rb`
|
|
107
|
+
2. Ensure that `rake` passes (format and tests)
|
|
108
|
+
3. Tag a release and push to rubygems.org by running `rake release`
|
|
109
|
+
4. Push the new version to npmjs.org by running `yarn publish`
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
.kpop-container {
|
|
2
|
+
display: none;
|
|
3
|
+
position: fixed;
|
|
4
|
+
left: 0;
|
|
5
|
+
top: 0;
|
|
6
|
+
right: 0;
|
|
7
|
+
bottom: 0;
|
|
8
|
+
justify-content: center;
|
|
9
|
+
align-items: center;
|
|
10
|
+
z-index: 1000;
|
|
11
|
+
pointer-events: none;
|
|
12
|
+
}
|
|
13
|
+
.kpop-container > * {
|
|
14
|
+
pointer-events: auto;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.kpop-modal {
|
|
18
|
+
position: relative;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
display: grid;
|
|
21
|
+
grid-template-areas: "title-bar" "header" "content" "footer";
|
|
22
|
+
grid-template-rows: auto auto 1fr auto;
|
|
23
|
+
border: 1px solid black;
|
|
24
|
+
border-radius: 0.5rem;
|
|
25
|
+
background-color: white;
|
|
26
|
+
min-width: 35rem;
|
|
27
|
+
max-width: 52rem;
|
|
28
|
+
min-height: 0;
|
|
29
|
+
max-height: 80vh;
|
|
30
|
+
}
|
|
31
|
+
.kpop-modal .kpop-title-bar {
|
|
32
|
+
grid-area: title-bar;
|
|
33
|
+
display: flex;
|
|
34
|
+
background: #344055;
|
|
35
|
+
color: white;
|
|
36
|
+
}
|
|
37
|
+
.kpop-modal .kpop-title-bar .kpop-title {
|
|
38
|
+
padding: 1rem 1.5rem;
|
|
39
|
+
flex-grow: 1;
|
|
40
|
+
}
|
|
41
|
+
.kpop-modal .kpop-title-bar .kpop-close {
|
|
42
|
+
background: none;
|
|
43
|
+
border: none;
|
|
44
|
+
color: white;
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
display: block;
|
|
47
|
+
font-size: 2rem;
|
|
48
|
+
font-weight: bold;
|
|
49
|
+
padding: 0 0.75rem;
|
|
50
|
+
text-decoration: none;
|
|
51
|
+
}
|
|
52
|
+
.kpop-modal .kpop-header {
|
|
53
|
+
grid-area: header;
|
|
54
|
+
}
|
|
55
|
+
.kpop-modal .kpop-content {
|
|
56
|
+
grid-area: content;
|
|
57
|
+
display: flex;
|
|
58
|
+
flex-direction: column;
|
|
59
|
+
overflow: auto;
|
|
60
|
+
}
|
|
61
|
+
.kpop-modal .kpop-footer {
|
|
62
|
+
grid-area: footer;
|
|
63
|
+
background: white;
|
|
64
|
+
border-top: 1px solid black;
|
|
65
|
+
padding: 1rem 1.5rem;
|
|
66
|
+
}
|
|
67
|
+
.kpop-modal .kpop-buttons {
|
|
68
|
+
display: flex;
|
|
69
|
+
gap: 0.5rem;
|
|
70
|
+
justify-content: space-between;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.kpop-modal.iframe .kpop-content {
|
|
74
|
+
overflow: unset;
|
|
75
|
+
}
|
|
76
|
+
.kpop-modal.iframe iframe {
|
|
77
|
+
height: 80vh;
|
|
78
|
+
width: 52rem;
|
|
79
|
+
flex-grow: 1;
|
|
80
|
+
overflow: scroll;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@media (max-width: 600px), (max-height: 600px) {
|
|
84
|
+
.kpop-modal {
|
|
85
|
+
max-width: unset;
|
|
86
|
+
min-width: unset;
|
|
87
|
+
width: 100%;
|
|
88
|
+
height: 100%;
|
|
89
|
+
max-height: 100vh;
|
|
90
|
+
border-radius: 0;
|
|
91
|
+
border: none;
|
|
92
|
+
}
|
|
93
|
+
.kpop-modal.iframe iframe {
|
|
94
|
+
width: 100%;
|
|
95
|
+
height: 100%;
|
|
96
|
+
}
|
|
97
|
+
.kpop-buttons {
|
|
98
|
+
flex-direction: column-reverse;
|
|
99
|
+
text-align: center;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
.scrim {
|
|
103
|
+
position: fixed;
|
|
104
|
+
top: 0;
|
|
105
|
+
bottom: 0;
|
|
106
|
+
left: 0;
|
|
107
|
+
right: 0;
|
|
108
|
+
background: rgba(0, 0, 0, 0.6);
|
|
109
|
+
z-index: -1;
|
|
110
|
+
transition: opacity 0.2s ease-in-out, z-index 0.2s step-end;
|
|
111
|
+
opacity: 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.scrim[data-scrim-open-value=true] {
|
|
115
|
+
opacity: 1;
|
|
116
|
+
transition: opacity 0.2s ease-in-out, z-index 0.2s step-start;
|
|
117
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e){return e.replace(/(?:[_-])([a-z0-9])/g,((e,t)=>t.toUpperCase()))}function t(e){return e.charAt(0).toUpperCase()+e.slice(1)}function r(e,t){const r=s(e);return Array.from(r.reduce(((e,r)=>(function(e,t){const r=e[t];return Array.isArray(r)?r:[]}(r,t).forEach((t=>e.add(t))),e)),new Set))}function n(e,t){return s(e).reduce(((e,r)=>(e.push(...function(e,t){const r=e[t];return r?Object.keys(r).map((e=>[e,r[e]])):[]}(r,t)),e)),[])}function s(e){const t=[];for(;e;)t.push(e),e=Object.getPrototypeOf(e);return t.reverse()}function i(e){return e.reduce(((e,[t,r])=>Object.assign(Object.assign({},e),{[t]:r})),{})}function o([t,r],n){return function(t){const r=`${s=t.token,s.replace(/([A-Z])/g,((e,t)=>`-${t.toLowerCase()}`))}-value`,n=function(e){const t=function(e){const t=a(e.typeObject.type);if(!t)return;const r=c(e.typeObject.default);if(t!==r){const n=e.controller?`${e.controller}.${e.token}`:e.token;throw new Error(`The specified default value for the Stimulus Value "${n}" must match the defined type "${t}". The provided default value of "${e.typeObject.default}" is of type "${r}".`)}return t}({controller:e.controller,token:e.token,typeObject:e.typeDefinition}),r=c(e.typeDefinition),n=a(e.typeDefinition),s=t||r||n;if(s)return s;const i=e.controller?`${e.controller}.${e.typeDefinition}`:e.token;throw new Error(`Unknown value type "${i}" for "${e.token}" value`)}(t);var s;return{type:n,key:r,name:e(r),get defaultValue(){return function(e){const t=a(e);if(t)return u[t];const r=e.default;return void 0!==r?r:e}(t.typeDefinition)},get hasCustomDefaultValue(){return void 0!==c(t.typeDefinition)},reader:l[n],writer:d[n]||d.default}}({controller:n,token:t,typeDefinition:r})}function a(e){switch(e){case Array:return"array";case Boolean:return"boolean";case Number:return"number";case Object:return"object";case String:return"string"}}function c(e){switch(typeof e){case"boolean":return"boolean";case"number":return"number";case"string":return"string"}return Array.isArray(e)?"array":"[object Object]"===Object.prototype.toString.call(e)?"object":void 0}(()=>{function e(e){function t(){return Reflect.construct(e,arguments,new.target)}return t.prototype=Object.create(e.prototype,{constructor:{value:t}}),Reflect.setPrototypeOf(t,e),t}try{return function(){const t=e((function(){this.a.call(this)}));t.prototype.a=function(){},new t}(),e}catch(e){return e=>class extends e{}}})(),Object.assign(Object.assign({enter:"Enter",tab:"Tab",esc:"Escape",space:" ",up:"ArrowUp",down:"ArrowDown",left:"ArrowLeft",right:"ArrowRight",home:"Home",end:"End"},i("abcdefghijklmnopqrstuvwxyz".split("").map((e=>[e,e])))),i("0123456789".split("").map((e=>[e,e]))));const u={get array(){return[]},boolean:!1,number:0,get object(){return{}},string:""},l={array(e){const t=JSON.parse(e);if(!Array.isArray(t))throw new TypeError(`expected value of type "array" but instead got value "${e}" of type "${c(t)}"`);return t},boolean:e=>!("0"==e||"false"==String(e).toLowerCase()),number:e=>Number(e),object(e){const t=JSON.parse(e);if(null===t||"object"!=typeof t||Array.isArray(t))throw new TypeError(`expected value of type "object" but instead got value "${e}" of type "${c(t)}"`);return t},string:e=>e},d={default:function(e){return`${e}`},array:h,object:h};function h(e){return JSON.stringify(e)}class p{constructor(e){this.context=e}static get shouldLoad(){return!0}static afterLoad(e,t){}get application(){return this.context.application}get scope(){return this.context.scope}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get targets(){return this.scope.targets}get outlets(){return this.scope.outlets}get classes(){return this.scope.classes}get data(){return this.scope.data}initialize(){}connect(){}disconnect(){}dispatch(e,{target:t=this.element,detail:r={},prefix:n=this.identifier,bubbles:s=!0,cancelable:i=!0}={}){const o=new CustomEvent(n?`${n}:${e}`:e,{detail:r,bubbles:s,cancelable:i});return t.dispatchEvent(o),o}}p.blessings=[function(e){return r(e,"classes").reduce(((e,r)=>{return Object.assign(e,{[`${n=r}Class`]:{get(){const{classes:e}=this;if(e.has(n))return e.get(n);{const t=e.getAttributeName(n);throw new Error(`Missing attribute "${t}"`)}}},[`${n}Classes`]:{get(){return this.classes.getAll(n)}},[`has${t(n)}Class`]:{get(){return this.classes.has(n)}}});var n}),{})},function(e){return r(e,"targets").reduce(((e,r)=>{return Object.assign(e,{[`${n=r}Target`]:{get(){const e=this.targets.find(n);if(e)return e;throw new Error(`Missing target element "${n}" for "${this.identifier}" controller`)}},[`${n}Targets`]:{get(){return this.targets.findAll(n)}},[`has${t(n)}Target`]:{get(){return this.targets.has(n)}}});var n}),{})},function(e){const r=n(e,"values"),s={valueDescriptorMap:{get(){return r.reduce(((e,t)=>{const r=o(t,this.identifier),n=this.data.getAttributeNameForKey(r.key);return Object.assign(e,{[n]:r})}),{})}}};return r.reduce(((e,r)=>Object.assign(e,function(e,r){const n=o(e,r),{key:s,name:i,reader:a,writer:c}=n;return{[i]:{get(){const e=this.data.get(s);return null!==e?a(e):n.defaultValue},set(e){void 0===e?this.data.delete(s):this.data.set(s,c(e))}},[`has${t(i)}`]:{get(){return this.data.has(s)||n.hasCustomDefaultValue}}}}(r))),s)},function(n){return r(n,"outlets").reduce(((r,n)=>Object.assign(r,function(r){const n=(s=r,e(s.replace(/--/g,"-").replace(/__/g,"_")));var s;return{[`${n}Outlet`]:{get(){const e=this.outlets.find(r);if(e){const t=this.application.getControllerForElementAndIdentifier(e,r);if(t)return t;throw new Error(`Missing "data-controller=${r}" attribute on outlet element for "${this.identifier}" controller`)}throw new Error(`Missing outlet element "${r}" for "${this.identifier}" controller`)}},[`${n}Outlets`]:{get(){const e=this.outlets.findAll(r);return e.length>0?e.map((e=>{const t=this.application.getControllerForElementAndIdentifier(e,r);if(t)return t;console.warn(`The provided outlet element is missing the outlet controller "${r}" for "${this.identifier}"`,e)})).filter((e=>e)):[]}},[`${n}OutletElement`]:{get(){const e=this.outlets.find(r);if(e)return e;throw new Error(`Missing outlet element "${r}" for "${this.identifier}" controller`)}},[`${n}OutletElements`]:{get(){return this.outlets.findAll(r)}},[`has${t(n)}Outlet`]:{get(){return this.outlets.has(r)}}}}(n))),{})}],p.targets=[],p.outlets=[],p.values={};class f extends p{static values={open:Boolean,captive:Boolean,zIndex:Number};static showScrim({dismiss:e=!0,zIndex:t,top:r}={}){return window.dispatchEvent(new CustomEvent("scrim:request:show",{cancelable:!0,detail:{captive:!e,zIndex:t,top:r}}))}static hideScrim(){return window.dispatchEvent(new CustomEvent("scrim:request:hide",{cancelable:!0}))}connect(){this.defaultZIndexValue=this.zIndexValue,this.defaultCaptiveValue=this.captiveValue}show(e){if(this.openValue&&this.hide(e),this.openValue)return;this.openValue=!0;if(this.dispatch("show",{bubbles:!0,cancelable:!0}).defaultPrevented)return this.openValue=!1,void e.preventDefault();this.#e(e.detail)}hide(e){if(!this.openValue)return;this.openValue=!1;if(this.dispatch("hide",{bubbles:!0,cancelable:!0}).defaultPrevented)return this.openValue=!0,void e.preventDefault();this.#t()}dismiss(e){this.captiveValue||this.hide(e)}escape(e){"Escape"!==e.key||this.captiveValue||e.defaultPrevented||this.hide(e)}disconnect(){super.disconnect()}#e({captive:e=this.defaultCaptiveValue,zIndex:t=this.defaultZIndexValue,top:r=window.scrollY}){this.captiveValue=e,this.zIndexValue=t,this.scrollY=r,this.previousPosition=document.body.style.position,this.previousTop=document.body.style.top,this.element.style.zIndex=this.zIndexValue,document.body.style.top=`-${r}px`,document.body.style.position="fixed"}#t(){this.captiveValue=this.defaultCaptiveValue,this.zIndexValue=this.defaultZIndexValue,g(this.element,"z-index",null),g(document.body,"position",null),g(document.body,"top",null),window.scrollTo(0,this.scrollY),delete this.scrollY,delete this.previousPosition,delete this.previousTop}}function g(e,t,r){r?e.style.setProperty(t,r):e.style.removeProperty(t)}class b extends p{static targets=["content","closeButton"];static values={open:Boolean};contentTargetConnected(){this.openValue||(f.showScrim({dismiss:this.hasCloseButtonTarget})?this.openValue=!0:this.#r())}contentTargetDisconnected(){this.hasContentTarget||(this.openValue=!1,f.hideScrim())}openValueChanged(e){this.element.style.display=e?"flex":"none"}dismiss(){if(!this.hasContentTarget||!this.openValue)return;const e=this.contentTarget.dataset.dismissUrl,t=this.contentTarget.dataset.dismissAction;e&&("replace"===t?!function(e,t){try{return`${new URL(e)}`==`${new URL(t,location.href)}`}catch{return!1}}(document.referrer,e)?history.replaceState({},"",e):history.back():window.location.href=e),this.#r()}#r(){this.element.removeAttribute("src"),this.element.innerHTML=""}}const y=[{identifier:"kpop",controllerConstructor:b},{identifier:"scrim",controllerConstructor:f}];export{b as KpopController,f as ScrimController,y as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//= link_tree ../javascripts/controllers
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
import ScrimController from "./scrim_controller";
|
|
3
|
+
|
|
4
|
+
export default class KpopController extends Controller {
|
|
5
|
+
static targets = ["content", "closeButton"];
|
|
6
|
+
static values = {
|
|
7
|
+
open: Boolean,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
contentTargetConnected() {
|
|
11
|
+
// When switching modals a target may connect while scrim is already open
|
|
12
|
+
if (this.openValue) return;
|
|
13
|
+
|
|
14
|
+
if (ScrimController.showScrim({ dismiss: this.hasCloseButtonTarget })) {
|
|
15
|
+
this.openValue = true;
|
|
16
|
+
} else {
|
|
17
|
+
this.#clear();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
contentTargetDisconnected() {
|
|
22
|
+
// When switching modals there may still be content to show
|
|
23
|
+
if (this.hasContentTarget) return;
|
|
24
|
+
|
|
25
|
+
this.openValue = false;
|
|
26
|
+
ScrimController.hideScrim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
openValueChanged(open) {
|
|
30
|
+
this.element.style.display = open ? "flex" : "none";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
dismiss() {
|
|
34
|
+
if (!this.hasContentTarget || !this.openValue) return;
|
|
35
|
+
|
|
36
|
+
const dismissUrl = this.contentTarget.dataset.dismissUrl;
|
|
37
|
+
const dismissAction = this.contentTarget.dataset.dismissAction;
|
|
38
|
+
|
|
39
|
+
if (dismissUrl) {
|
|
40
|
+
if (dismissAction === "replace") {
|
|
41
|
+
if (isSameUrl(document.referrer, dismissUrl)) {
|
|
42
|
+
// if we came from the same page, send the user back
|
|
43
|
+
history.back();
|
|
44
|
+
} else {
|
|
45
|
+
// if we came from a different page, dismiss the modal and replace url
|
|
46
|
+
history.replaceState({}, "", dismissUrl);
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
// default, send the user on to the specified URL
|
|
50
|
+
window.location.href = dismissUrl;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.#clear();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#clear() {
|
|
58
|
+
this.element.removeAttribute("src");
|
|
59
|
+
this.element.innerHTML = "";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isSameUrl(previous, next) {
|
|
64
|
+
try {
|
|
65
|
+
return `${new URL(previous)}` === `${new URL(next, location.href)}`;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
|
|
3
|
+
const DEBUG = false;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Scrim controller wraps an element that creates a whole page layer.
|
|
7
|
+
* It is intended to be used behind a modal or nav drawer.
|
|
8
|
+
*
|
|
9
|
+
* If the Scrim element receives a click event, it automatically triggers "scrim:hide".
|
|
10
|
+
*
|
|
11
|
+
* You can show and hide the scrim programmatically by sending "scrim:request:show" and "scrim:request:hide" events to
|
|
12
|
+
* the window or by calling the provided methods.
|
|
13
|
+
*
|
|
14
|
+
* If you need to respond to the scrim showing or hiding you should subscribe to "scrim:show" and "scrim:hide".
|
|
15
|
+
*/
|
|
16
|
+
export default class ScrimController extends Controller {
|
|
17
|
+
static values = {
|
|
18
|
+
open: Boolean,
|
|
19
|
+
captive: Boolean,
|
|
20
|
+
zIndex: Number,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Show the scrim element. Returns true if successful.
|
|
25
|
+
*/
|
|
26
|
+
static showScrim({
|
|
27
|
+
dismiss = true,
|
|
28
|
+
zIndex = undefined,
|
|
29
|
+
top = undefined,
|
|
30
|
+
} = {}) {
|
|
31
|
+
return window.dispatchEvent(
|
|
32
|
+
new CustomEvent("scrim:request:show", {
|
|
33
|
+
cancelable: true,
|
|
34
|
+
detail: { captive: !dismiss, zIndex: zIndex, top: top },
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Hide the scrim element. Returns true if successful.
|
|
41
|
+
*/
|
|
42
|
+
static hideScrim() {
|
|
43
|
+
return window.dispatchEvent(
|
|
44
|
+
new CustomEvent("scrim:request:hide", { cancelable: true })
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
connect() {
|
|
49
|
+
this.defaultZIndexValue = this.zIndexValue;
|
|
50
|
+
this.defaultCaptiveValue = this.captiveValue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
show(request) {
|
|
54
|
+
if (DEBUG) console.debug("request show scrim");
|
|
55
|
+
|
|
56
|
+
// hide the scrim before opening the new one if it's already open
|
|
57
|
+
if (this.openValue) this.hide(request);
|
|
58
|
+
|
|
59
|
+
// if the scrim is still open, abort
|
|
60
|
+
if (this.openValue) return;
|
|
61
|
+
|
|
62
|
+
// update internal state to break event cycles
|
|
63
|
+
this.openValue = true;
|
|
64
|
+
|
|
65
|
+
// notify listeners of pending request
|
|
66
|
+
const event = this.dispatch("show", { bubbles: true, cancelable: true });
|
|
67
|
+
|
|
68
|
+
// if notification was cancelled, update request and abort
|
|
69
|
+
if (event.defaultPrevented) {
|
|
70
|
+
this.openValue = false;
|
|
71
|
+
request.preventDefault();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (DEBUG) console.debug("show scrim");
|
|
76
|
+
|
|
77
|
+
// perform show updates
|
|
78
|
+
this.#show(request.detail);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
hide(request) {
|
|
82
|
+
if (!this.openValue) return;
|
|
83
|
+
|
|
84
|
+
if (DEBUG) console.debug("request hide scrim");
|
|
85
|
+
|
|
86
|
+
// update internal state to break event cycles
|
|
87
|
+
this.openValue = false;
|
|
88
|
+
|
|
89
|
+
// notify listeners of pending request
|
|
90
|
+
const event = this.dispatch("hide", { bubbles: true, cancelable: true });
|
|
91
|
+
|
|
92
|
+
// if notification was cancelled, update request and abort
|
|
93
|
+
if (event.defaultPrevented) {
|
|
94
|
+
this.openValue = true;
|
|
95
|
+
request.preventDefault();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (DEBUG) console.debug("hide scrim");
|
|
100
|
+
|
|
101
|
+
// update state, perform style updates
|
|
102
|
+
this.#hide();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
dismiss(event) {
|
|
106
|
+
if (!this.captiveValue) this.hide(event);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
escape(event) {
|
|
110
|
+
if (event.key === "Escape" && !this.captiveValue && !event.defaultPrevented)
|
|
111
|
+
this.hide(event);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
disconnect() {
|
|
115
|
+
super.disconnect();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Clips body to viewport size and sets the z-index
|
|
120
|
+
*/
|
|
121
|
+
#show({
|
|
122
|
+
captive = this.defaultCaptiveValue,
|
|
123
|
+
zIndex = this.defaultZIndexValue,
|
|
124
|
+
top = window.scrollY,
|
|
125
|
+
}) {
|
|
126
|
+
this.captiveValue = captive;
|
|
127
|
+
this.zIndexValue = zIndex;
|
|
128
|
+
this.scrollY = top;
|
|
129
|
+
|
|
130
|
+
this.previousPosition = document.body.style.position;
|
|
131
|
+
this.previousTop = document.body.style.top;
|
|
132
|
+
|
|
133
|
+
this.element.style.zIndex = this.zIndexValue;
|
|
134
|
+
document.body.style.top = `-${top}px`;
|
|
135
|
+
document.body.style.position = "fixed";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Unclips body from viewport size and unsets the z-index
|
|
140
|
+
*/
|
|
141
|
+
#hide() {
|
|
142
|
+
this.captiveValue = this.defaultCaptiveValue;
|
|
143
|
+
this.zIndexValue = this.defaultZIndexValue;
|
|
144
|
+
|
|
145
|
+
resetStyle(this.element, "z-index", null);
|
|
146
|
+
resetStyle(document.body, "position", null);
|
|
147
|
+
resetStyle(document.body, "top", null);
|
|
148
|
+
window.scrollTo(0, this.scrollY);
|
|
149
|
+
|
|
150
|
+
delete this.scrollY;
|
|
151
|
+
delete this.previousPosition;
|
|
152
|
+
delete this.previousTop;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resetStyle(element, property, previousValue) {
|
|
157
|
+
if (previousValue) {
|
|
158
|
+
element.style.setProperty(property, previousValue);
|
|
159
|
+
} else {
|
|
160
|
+
element.style.removeProperty(property);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import KpopController from "../controllers/kpop_controller";
|
|
2
|
+
import ScrimController from "../controllers/scrim_controller";
|
|
3
|
+
|
|
4
|
+
const Definitions = [
|
|
5
|
+
{ identifier: "kpop", controllerConstructor: KpopController },
|
|
6
|
+
{ identifier: "scrim", controllerConstructor: ScrimController },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export { Definitions as default, KpopController, ScrimController };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
$title-background-color: #344055 !default;
|
|
2
|
+
$title-text-color: white !default;
|
|
3
|
+
$min-width: 35rem !default;
|
|
4
|
+
$max-width: 52rem !default;
|
|
5
|
+
$min-height: 0 !default;
|
|
6
|
+
$max-height: 80vh !default;
|
|
7
|
+
$default-padding: 1rem 1.5rem !default;
|
|
8
|
+
|
|
9
|
+
.kpop-container {
|
|
10
|
+
display: none;
|
|
11
|
+
|
|
12
|
+
position: fixed;
|
|
13
|
+
left: 0;
|
|
14
|
+
top: 0;
|
|
15
|
+
right: 0;
|
|
16
|
+
bottom: 0;
|
|
17
|
+
|
|
18
|
+
justify-content: center;
|
|
19
|
+
align-items: center;
|
|
20
|
+
z-index: 1000;
|
|
21
|
+
pointer-events: none;
|
|
22
|
+
|
|
23
|
+
> * {
|
|
24
|
+
pointer-events: auto;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.kpop-modal {
|
|
29
|
+
position: relative;
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
|
|
32
|
+
display: grid;
|
|
33
|
+
grid-template-areas:
|
|
34
|
+
"title-bar"
|
|
35
|
+
"header"
|
|
36
|
+
"content"
|
|
37
|
+
"footer";
|
|
38
|
+
grid-template-rows: auto auto 1fr auto;
|
|
39
|
+
|
|
40
|
+
border: 1px solid black;
|
|
41
|
+
border-radius: 0.5rem;
|
|
42
|
+
background-color: white;
|
|
43
|
+
|
|
44
|
+
min-width: $min-width;
|
|
45
|
+
max-width: $max-width;
|
|
46
|
+
min-height: $min-height;
|
|
47
|
+
max-height: $max-height;
|
|
48
|
+
|
|
49
|
+
.kpop-title-bar {
|
|
50
|
+
grid-area: title-bar;
|
|
51
|
+
display: flex;
|
|
52
|
+
background: $title-background-color;
|
|
53
|
+
color: $title-text-color;
|
|
54
|
+
|
|
55
|
+
.kpop-title {
|
|
56
|
+
padding: $default-padding;
|
|
57
|
+
flex-grow: 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.kpop-close {
|
|
61
|
+
background: none;
|
|
62
|
+
border: none;
|
|
63
|
+
color: $title-text-color;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
display: block;
|
|
66
|
+
font-size: 2rem;
|
|
67
|
+
font-weight: bold;
|
|
68
|
+
padding: 0 0.75rem;
|
|
69
|
+
text-decoration: none;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.kpop-header {
|
|
74
|
+
grid-area: header;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.kpop-content {
|
|
78
|
+
grid-area: content;
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
overflow: auto;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.kpop-footer {
|
|
85
|
+
grid-area: footer;
|
|
86
|
+
background: white;
|
|
87
|
+
border-top: 1px solid black;
|
|
88
|
+
padding: $default-padding;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.kpop-buttons {
|
|
92
|
+
display: flex;
|
|
93
|
+
gap: 0.5rem;
|
|
94
|
+
justify-content: space-between;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.kpop-modal.iframe {
|
|
99
|
+
.kpop-content {
|
|
100
|
+
overflow: unset;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
iframe {
|
|
104
|
+
height: $max-height;
|
|
105
|
+
width: $max-width;
|
|
106
|
+
flex-grow: 1;
|
|
107
|
+
overflow: scroll;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@media (max-width: 600px), (max-height: 600px) {
|
|
112
|
+
.kpop-modal {
|
|
113
|
+
max-width: unset;
|
|
114
|
+
min-width: unset;
|
|
115
|
+
width: 100%;
|
|
116
|
+
height: 100%;
|
|
117
|
+
max-height: 100vh;
|
|
118
|
+
border-radius: 0;
|
|
119
|
+
border: none;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.kpop-modal.iframe {
|
|
123
|
+
iframe {
|
|
124
|
+
width: 100%;
|
|
125
|
+
height: 100%;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.kpop-buttons {
|
|
130
|
+
flex-direction: column-reverse;
|
|
131
|
+
text-align: center;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
.scrim {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
bottom: 0;
|
|
5
|
+
left: 0;
|
|
6
|
+
right: 0;
|
|
7
|
+
background: rgba(0, 0, 0, 0.6);
|
|
8
|
+
z-index: -1;
|
|
9
|
+
transition: opacity 0.2s ease-in-out, z-index 0.2s step-end;
|
|
10
|
+
opacity: 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.scrim[data-scrim-open-value="true"] {
|
|
14
|
+
opacity: 1;
|
|
15
|
+
transition: opacity 0.2s ease-in-out, z-index 0.2s step-start;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@use "kpop/index";
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Rails/HelperInstanceVariable
|
|
4
|
+
module Kpop
|
|
5
|
+
class Modal
|
|
6
|
+
delegate_missing_to :@context
|
|
7
|
+
|
|
8
|
+
def initialize(context)
|
|
9
|
+
@context = context
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def render(options = {})
|
|
13
|
+
dom_class = options.delete(:class)
|
|
14
|
+
|
|
15
|
+
# Generate a title bar. This can be overridden by calling title_bar again.
|
|
16
|
+
title_bar(options) unless options.fetch(:title, "").nil?
|
|
17
|
+
|
|
18
|
+
# Render block. This may have side-effect writes to header/content/footer
|
|
19
|
+
# etc. If @content is set then this value will be ignored.
|
|
20
|
+
content = capture do
|
|
21
|
+
yield self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
tag.div(class: class_names("kpop-modal", dom_class),
|
|
25
|
+
data: kpop_data_options(options),
|
|
26
|
+
**options) do
|
|
27
|
+
concat @title_bar
|
|
28
|
+
concat @header if @header.present?
|
|
29
|
+
concat @content.presence || tag.div(content, class: "kpop-content")
|
|
30
|
+
concat @footer if @footer.present?
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Generates a sticky title bar for the modal. Content should not be too long
|
|
35
|
+
# as the bar does not provide wrapping.
|
|
36
|
+
def title_bar(options = {}, &block)
|
|
37
|
+
title = options.delete(:title)
|
|
38
|
+
captive = options.delete(:captive)
|
|
39
|
+
@title_bar = tag.div(class: "kpop-title-bar", **options) do
|
|
40
|
+
concat(tag.span(class: "kpop-title") do
|
|
41
|
+
concat(block ? (yield self) : title)
|
|
42
|
+
end)
|
|
43
|
+
concat(close_icon) unless captive
|
|
44
|
+
end
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Generates sticky header content for the top of the modal. Content is not
|
|
49
|
+
# padded, if you want padding you should provide a padding class.
|
|
50
|
+
def header(**options, &block)
|
|
51
|
+
modal_content(:header, **options, &block)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Generates content for the modal. Content is not padded, if you want
|
|
55
|
+
# padding you should provide a padding class.
|
|
56
|
+
def content(**options, &block)
|
|
57
|
+
modal_content(:content, **options, &block)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Generates a sticky footer element at the bottom of the modal.
|
|
61
|
+
# Footer is padded and contents are assumed to be buttons.
|
|
62
|
+
def footer(**options, &block)
|
|
63
|
+
modal_content(:footer, **options, &block)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def close_icon
|
|
67
|
+
tag.button(
|
|
68
|
+
"×",
|
|
69
|
+
class: "kpop-close",
|
|
70
|
+
data: {
|
|
71
|
+
kpop_target: "closeButton",
|
|
72
|
+
action: "click->kpop#dismiss:prevent",
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def kpop_data_options(options)
|
|
80
|
+
data = options.delete(:data) || {}
|
|
81
|
+
data.reverse_merge(
|
|
82
|
+
kpop_target: "content",
|
|
83
|
+
dismiss_action: options.delete(:dismiss_action),
|
|
84
|
+
dismiss_url: options.delete(:dismiss_url),
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def class_for(name, options)
|
|
89
|
+
class_names("kpop-#{name}", options.delete(:class))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def modal_content(name, **options, &block)
|
|
93
|
+
instance_variable_set("@#{name}", tag.div(class: class_for(name, options), **options, &block))
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
# rubocop:enable Rails/HelperInstanceVariable
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KpopHelper
|
|
4
|
+
def render_kpop(options = {}, &block)
|
|
5
|
+
Kpop::Modal.new(self).render(options, &block)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def kpop_frame_tag(&block)
|
|
9
|
+
turbo_frame_tag "kpop",
|
|
10
|
+
class: "kpop-container",
|
|
11
|
+
data: { controller: "kpop", action: "scrim:hide@window->kpop#dismiss" } do
|
|
12
|
+
capture(&block) if block
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def kpop_link_to(name = nil, options = nil, html_options = nil, &block)
|
|
17
|
+
default_html_options = {
|
|
18
|
+
data: { turbo: true, turbo_frame: "kpop" },
|
|
19
|
+
}
|
|
20
|
+
if block
|
|
21
|
+
# Param[name] is the path for the link
|
|
22
|
+
link_to(name, default_html_options.deep_merge(options || {}), &block)
|
|
23
|
+
else
|
|
24
|
+
link_to(name, options, default_html_options.deep_merge(html_options || {}))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def kpop_button_to(name = nil, options = nil, html_options = nil, &block)
|
|
29
|
+
default_html_options = {
|
|
30
|
+
form: { data: { turbo: true, turbo_frame: "kpop" } },
|
|
31
|
+
}
|
|
32
|
+
button_to(name, options, default_html_options.deep_merge(html_options || {}), &block)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def kpop_button_close(content = nil, **options, &block)
|
|
36
|
+
content = block ? capture(yield) : content
|
|
37
|
+
tag.button content, data: { action: "click->kpop#dismiss:prevent" }, **options
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScrimHelper
|
|
4
|
+
def scrim_tag(z_index: 40)
|
|
5
|
+
tag.div(class: "scrim", data: { controller: "scrim", scrim_z_index_value: z_index, action: <<~ACTIONS })
|
|
6
|
+
click->scrim#dismiss
|
|
7
|
+
keyup@window->scrim#escape
|
|
8
|
+
scrim:request:hide@window->scrim#hide
|
|
9
|
+
scrim:request:show@window->scrim#show
|
|
10
|
+
ACTIONS
|
|
11
|
+
end
|
|
12
|
+
end
|
data/config/importmap.rb
ADDED
data/config/routes.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails"
|
|
4
|
+
|
|
5
|
+
module Katalyst
|
|
6
|
+
module Kpop
|
|
7
|
+
class Engine < ::Rails::Engine
|
|
8
|
+
config.autoload_once_paths = %W(#{root}/app/helpers)
|
|
9
|
+
|
|
10
|
+
initializer "kpop.helpers", before: :load_config_initializers do
|
|
11
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
12
|
+
helper Katalyst::Kpop::Engine.helpers
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
ActiveSupport.on_load(:action_view_base) do
|
|
16
|
+
helper Katalyst::Kpop::Engine.helpers
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
initializer "kpop.assets" do
|
|
21
|
+
config.after_initialize do |app|
|
|
22
|
+
if app.config.respond_to?(:assets)
|
|
23
|
+
app.config.assets.precompile += %w(kpop.js)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
initializer "kpop.importmap", before: "importmap" do |app|
|
|
29
|
+
if app.config.respond_to?(:importmap)
|
|
30
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
31
|
+
app.config.importmap.cache_sweepers << root.join("app/assets/javascripts")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/tasks/yarn.rake
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :yarn do
|
|
4
|
+
desc "Install npm packages with yarn"
|
|
5
|
+
task install: :environment do
|
|
6
|
+
sh "yarn install"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
desc "Lint JS/SCSS files using yarn (prettier)"
|
|
10
|
+
task lint: :install do
|
|
11
|
+
sh "yarn lint"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
desc "Autoformat JS/SCSS files using yarn (prettier)"
|
|
15
|
+
task format: :install do
|
|
16
|
+
sh "yarn format"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
desc "Compile js/css"
|
|
20
|
+
task build: :install do
|
|
21
|
+
sh "yarn build && yarn build:css"
|
|
22
|
+
end
|
|
23
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: katalyst-kpop
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 2.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Katalyst Interactive
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2023-04-05 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description:
|
|
14
|
+
email:
|
|
15
|
+
- developers@katalyst.com.au
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- README.md
|
|
21
|
+
- app/assets/builds/katalyst/kpop.css
|
|
22
|
+
- app/assets/builds/katalyst/kpop.min.js
|
|
23
|
+
- app/assets/config/kpop.js
|
|
24
|
+
- app/assets/javascripts/controllers/kpop_controller.js
|
|
25
|
+
- app/assets/javascripts/controllers/scrim_controller.js
|
|
26
|
+
- app/assets/javascripts/katalyst/kpop.js
|
|
27
|
+
- app/assets/stylesheets/katalyst/kpop.scss
|
|
28
|
+
- app/assets/stylesheets/katalyst/kpop/_index.scss
|
|
29
|
+
- app/assets/stylesheets/katalyst/kpop/_kpop.scss
|
|
30
|
+
- app/assets/stylesheets/katalyst/kpop/_scrim.scss
|
|
31
|
+
- app/helpers/kpop/modal.rb
|
|
32
|
+
- app/helpers/kpop_helper.rb
|
|
33
|
+
- app/helpers/scrim_helper.rb
|
|
34
|
+
- app/views/layouts/kpop.html.erb
|
|
35
|
+
- config/importmap.rb
|
|
36
|
+
- config/routes.rb
|
|
37
|
+
- lib/katalyst/kpop.rb
|
|
38
|
+
- lib/katalyst/kpop/engine.rb
|
|
39
|
+
- lib/katalyst/kpop/version.rb
|
|
40
|
+
- lib/tasks/yarn.rake
|
|
41
|
+
homepage: https://github.com/katalyst/kpop
|
|
42
|
+
licenses:
|
|
43
|
+
- MIT
|
|
44
|
+
metadata:
|
|
45
|
+
rubygems_mfa_required: 'true'
|
|
46
|
+
post_install_message:
|
|
47
|
+
rdoc_options: []
|
|
48
|
+
require_paths:
|
|
49
|
+
- lib
|
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.0'
|
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0'
|
|
60
|
+
requirements: []
|
|
61
|
+
rubygems_version: 3.4.10
|
|
62
|
+
signing_key:
|
|
63
|
+
specification_version: 4
|
|
64
|
+
summary: Modal library that uses Turbo and Stimulus.
|
|
65
|
+
test_files: []
|