satis 1.0.66
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/MIT-LICENSE +20 -0
- data/README.md +92 -0
- data/Rakefile +23 -0
- data/app/assets/config/satis_manifest.js +1 -0
- data/app/assets/stylesheets/satis/application.css +15 -0
- data/app/components/satis/appearance_switcher/component.html.slim +6 -0
- data/app/components/satis/appearance_switcher/component.rb +11 -0
- data/app/components/satis/appearance_switcher/component.scss +34 -0
- data/app/components/satis/appearance_switcher/component_controller.js +62 -0
- data/app/components/satis/application_component.rb +50 -0
- data/app/components/satis/avatar/component.html.slim +7 -0
- data/app/components/satis/avatar/component.rb +52 -0
- data/app/components/satis/breadcrumbs/component.html.slim +8 -0
- data/app/components/satis/breadcrumbs/component.rb +23 -0
- data/app/components/satis/breadcrumbs/component.scss +19 -0
- data/app/components/satis/breadcrumbs/crumb.slim +8 -0
- data/app/components/satis/card/component.html.slim +54 -0
- data/app/components/satis/card/component.md +14 -0
- data/app/components/satis/card/component.rb +41 -0
- data/app/components/satis/card/component.scss +15 -0
- data/app/components/satis/date_time_picker/component.html.slim +48 -0
- data/app/components/satis/date_time_picker/component.md +11 -0
- data/app/components/satis/date_time_picker/component.rb +48 -0
- data/app/components/satis/date_time_picker/component.scss +5 -0
- data/app/components/satis/date_time_picker/component_controller.js +499 -0
- data/app/components/satis/dropdown/component.html.slim +36 -0
- data/app/components/satis/dropdown/component.md +48 -0
- data/app/components/satis/dropdown/component.rb +77 -0
- data/app/components/satis/dropdown/component.scss +10 -0
- data/app/components/satis/dropdown/component_controller.js +547 -0
- data/app/components/satis/flash_messages/component.html.slim +3 -0
- data/app/components/satis/flash_messages/component.rb +31 -0
- data/app/components/satis/flash_messages/component.scss +18 -0
- data/app/components/satis/flash_messages/message.html.slim +8 -0
- data/app/components/satis/info/component.html.slim +4 -0
- data/app/components/satis/info/component.rb +22 -0
- data/app/components/satis/info_item/component.html.slim +7 -0
- data/app/components/satis/info_item/component.rb +19 -0
- data/app/components/satis/input/component.html.slim +11 -0
- data/app/components/satis/input/component.rb +38 -0
- data/app/components/satis/input/component.scss +50 -0
- data/app/components/satis/input/element.html.slim +2 -0
- data/app/components/satis/map/component.html.slim +2 -0
- data/app/components/satis/map/component.rb +17 -0
- data/app/components/satis/map/component.scss +9 -0
- data/app/components/satis/map/component_controller.js +37 -0
- data/app/components/satis/menu/component.html.slim +13 -0
- data/app/components/satis/menu/component.md +1 -0
- data/app/components/satis/menu/component.rb +16 -0
- data/app/components/satis/menu/component_controller.js +62 -0
- data/app/components/satis/menu_item/component.html.slim +16 -0
- data/app/components/satis/menu_item/component.rb +14 -0
- data/app/components/satis/page/component.html.slim +45 -0
- data/app/components/satis/page/component.rb +15 -0
- data/app/components/satis/page/component_controller.js +86 -0
- data/app/components/satis/sidebar_menu/component.html.slim +3 -0
- data/app/components/satis/sidebar_menu/component.rb +17 -0
- data/app/components/satis/sidebar_menu/component.scss +0 -0
- data/app/components/satis/sidebar_menu/component_controller.js +9 -0
- data/app/components/satis/sidebar_menu/mobile/component.html.slim +3 -0
- data/app/components/satis/sidebar_menu/mobile/component.rb +10 -0
- data/app/components/satis/sidebar_menu_item/component.html.slim +15 -0
- data/app/components/satis/sidebar_menu_item/component.rb +20 -0
- data/app/components/satis/sidebar_menu_item/component.scss +27 -0
- data/app/components/satis/sidebar_menu_item/component_controller.js +62 -0
- data/app/components/satis/sidebar_menu_item/mobile/component.html.slim +17 -0
- data/app/components/satis/sidebar_menu_item/mobile/component.rb +10 -0
- data/app/components/satis/switch/component.html.slim +14 -0
- data/app/components/satis/switch/component.rb +24 -0
- data/app/components/satis/switch/component_controller.js +49 -0
- data/app/components/satis/tab/component.rb +35 -0
- data/app/components/satis/tabs/component.html.slim +23 -0
- data/app/components/satis/tabs/component.md +21 -0
- data/app/components/satis/tabs/component.rb +16 -0
- data/app/components/satis/tabs/component.scss +33 -0
- data/app/components/satis/tabs/component_controller.js +123 -0
- data/app/controllers/satis/application_controller.rb +4 -0
- data/app/helpers/satis/application_helper.rb +15 -0
- data/app/jobs/satis/application_job.rb +4 -0
- data/app/mailers/satis/application_mailer.rb +6 -0
- data/app/models/satis/application_record.rb +5 -0
- data/app/views/shared/_fields_for.html.slim +35 -0
- data/config/routes.rb +5 -0
- data/lib/satis/action_controller_helpers.rb +29 -0
- data/lib/satis/configuration.rb +61 -0
- data/lib/satis/engine.rb +27 -0
- data/lib/satis/forms/builder.rb +440 -0
- data/lib/satis/forms/concerns/buttons.rb +49 -0
- data/lib/satis/forms/concerns/file.rb +35 -0
- data/lib/satis/forms/concerns/options.rb +44 -0
- data/lib/satis/forms/concerns/required.rb +68 -0
- data/lib/satis/forms/concerns/select.rb +95 -0
- data/lib/satis/helpers/container.rb +83 -0
- data/lib/satis/menus/builder.rb +13 -0
- data/lib/satis/menus/item.rb +34 -0
- data/lib/satis/menus/menu.rb +23 -0
- data/lib/satis/version.rb +3 -0
- data/lib/satis.rb +36 -0
- data/lib/tasks/satis_tasks.rake +4 -0
- metadata +213 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import ApplicationController from "../../../../frontend/controllers/application_controller"
|
|
2
|
+
// FIXME: Is this full path really needed?
|
|
3
|
+
import { debounce } from "../../../../frontend/utils"
|
|
4
|
+
|
|
5
|
+
export default class extends ApplicationController {
|
|
6
|
+
static targets = ["link", "indicator", "submenu"]
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
super.connect()
|
|
10
|
+
|
|
11
|
+
// Primitive, yes
|
|
12
|
+
Array.from(this.element.querySelectorAll('[data-satis-sidebar-menu-item-target="link"]')).forEach((el) => {
|
|
13
|
+
if (el.href.length > 0 && window.location.href.indexOf(el.href) >= 0) {
|
|
14
|
+
el.classList.add("active")
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
if (this.isActive) {
|
|
19
|
+
this.linkTarget.classList.add("active")
|
|
20
|
+
|
|
21
|
+
if (this.hasSubmenuTarget) {
|
|
22
|
+
this.submenuTarget.classList.remove("hidden")
|
|
23
|
+
this.indicatorTarget.setAttribute("data-fa-transform", "rotate-90")
|
|
24
|
+
} else {
|
|
25
|
+
this.linkTarget.classList.add("focus")
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
open(event) {
|
|
31
|
+
if (!this.isActive && this.hasSubmenuTarget) {
|
|
32
|
+
if (this.hasSubmenuTarget) {
|
|
33
|
+
this.submenuTarget.classList.remove("hidden")
|
|
34
|
+
this.indicatorTarget.setAttribute("data-fa-transform", "rotate-90")
|
|
35
|
+
}
|
|
36
|
+
event.preventDefault()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get linkInUrl() {
|
|
41
|
+
return this.linkTarget.href.length > 0 && window.location.href.indexOf(this.linkTarget.href) >= 0
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get isActive() {
|
|
45
|
+
return this.linkInUrl || this.hasOpenSubmenus || this.hasActiveLinks
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get hasOpenSubmenus() {
|
|
49
|
+
return Array.from(this.element.querySelectorAll('[data-satis-sidebar-menu-item-target="submenu"]')).some((el) => {
|
|
50
|
+
return !el.classList.contains("hidden")
|
|
51
|
+
// return Array.from(el.querySelectorAll('[data-satis-sidebar-menu-item-target="submenu"]')).some((el) => {
|
|
52
|
+
// return !el.classList.contains("hidden")
|
|
53
|
+
// })
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get hasActiveLinks() {
|
|
58
|
+
return Array.from(this.element.querySelectorAll('[data-satis-sidebar-menu-item-target="link"]')).some((el) => {
|
|
59
|
+
return el.classList.contains("active")
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.space-y-1 class="#{item.level >= 1 ? 'pl-4' : ''}"
|
|
2
|
+
a.text-gray-600.hover:bg-gray-50.hover:text-gray-900.group.flex.items-center.px-2.py-2.text-base.font-medium.rounded-md href=item.link *item.link_attributes
|
|
3
|
+
- if item.icon
|
|
4
|
+
i.fa-lg.text-gray-400.group-hover:text-gray-500.mr-4.flex-shrink-0.h-6.w-6 class=item.icon style="width: 20px;"
|
|
5
|
+
- else
|
|
6
|
+
i.mr-3.flex-shrink-0.h-6 style="width: 20px;"
|
|
7
|
+
span.flex-1
|
|
8
|
+
= item.label
|
|
9
|
+
|
|
10
|
+
/! Expanded: "text-gray-400 rotate-90", Collapsed: "text-gray-300"
|
|
11
|
+
- if item.menu
|
|
12
|
+
i.fal.fa-angle-right.text-gray-300.ml-3.flex-shrink-0.h-5.w-5.transform.group-hover:text-gray-400.transition-colors.ease-in-out.duration-150 style="width: 20px;"
|
|
13
|
+
|
|
14
|
+
- if item.menu
|
|
15
|
+
#sub-menu-1.space-y-1
|
|
16
|
+
- item.menu.items.each do |item|
|
|
17
|
+
= render(Satis::SidebarMenuItem::Mobile::Component.new(item: item))
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
div.satis-switch data-controller='satis-switch'
|
|
2
|
+
- if options[:label] != false
|
|
3
|
+
= form.custom_label(attribute, options[:label], data: { action: "click->satis-switch#toggle" })
|
|
4
|
+
= form.hidden_field(attribute, options[:input_html].reverse_merge(value: @value ? "1" : "0", 'data-action' => 'change->satis-switch#update'))
|
|
5
|
+
button.mt-3.mb-3.relative.inline-flex.flex-shrink-0.h-6.w-11.border-2.border-transparent.rounded-full.cursor-pointer.transition-colors.ease-in-out.duration-200.focus:outline-none.focus:ring-2.focus:ring-offset-2.focus:ring-primary-500 aria-checked="false" role="switch" type="button" data-action="click->satis-switch#toggle" data-satis-switch-target="button" class="#{@value ? 'bg-primary-600' : 'bg-gray-200'}"
|
|
6
|
+
span.pointer-events-none.inline-block.h-5.w-5.rounded-full.bg-white.shadow.transform.ring-0.transition.ease-in-out.duration-200 aria-hidden="true" data-satis-switch-target="switch" class="#{@value ? 'translate-x-5' : 'translate-x-0' }"
|
|
7
|
+
- if icon
|
|
8
|
+
span.absolute.inset-0.h-full.w-full.flex.items-center.justify-center.transition-opacity aria-hidden="true" data-satis-switch-target="cross" class="#{@value ? 'opacity-0 ease-out duration-100' : 'opacity-100 ease-in duration-200' }"
|
|
9
|
+
svg.h-3.w-3.text-gray-400 fill="none" viewbox=("0 0 12 12")
|
|
10
|
+
path d=("M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2") stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" /
|
|
11
|
+
span.absolute.inset-0.h-full.w-full.flex.items-center.justify-center.transition-opacity aria-hidden="true" data-satis-switch-target="check" class="#{@value ? 'opacity-100 ease-in duration-200' : 'opacity-0 ease-out duration-100' }"
|
|
12
|
+
svg.h-3.w-3.text-primary-600 fill="currentColor" viewbox=("0 0 12 12")
|
|
13
|
+
path d=("M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z") /
|
|
14
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Satis
|
|
4
|
+
module Switch
|
|
5
|
+
class Component < Satis::ApplicationComponent
|
|
6
|
+
attr_reader :url, :form, :attribute, :icon, :options
|
|
7
|
+
|
|
8
|
+
def initialize(form:, attribute:, **options, &block)
|
|
9
|
+
super
|
|
10
|
+
|
|
11
|
+
@form = form
|
|
12
|
+
@attribute = attribute
|
|
13
|
+
@options = options
|
|
14
|
+
@block = block
|
|
15
|
+
@icon = true
|
|
16
|
+
@icon = options[:icon] if options.key?(:icon)
|
|
17
|
+
@value = options.key?(:value) ? options[:value] : @form.object.send(attribute)
|
|
18
|
+
@value = @value == '0' || !@value ? false : true
|
|
19
|
+
options[:input_html] ||= {}
|
|
20
|
+
options[:input_html] = { data: { 'satis-switch-target' => 'hiddenInput' } }.deep_merge(options[:input_html])
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import ApplicationController from "../../../../frontend/controllers/application_controller"
|
|
2
|
+
// FIXME: Is this full path really needed?
|
|
3
|
+
import { debounce } from "../../../../frontend/utils"
|
|
4
|
+
|
|
5
|
+
export default class extends ApplicationController {
|
|
6
|
+
static targets = ["hiddenInput", "switch", "button", "cross", "check"]
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
super.connect()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
toggle(event) {
|
|
13
|
+
this.hiddenInputTarget.value = this.hiddenInputTarget.value == "1" ? "0" : "1"
|
|
14
|
+
this.hiddenInputTarget.dispatchEvent(new Event("change"))
|
|
15
|
+
this.update()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
update(event) {
|
|
19
|
+
if (this.hiddenInputTarget.value == "1") {
|
|
20
|
+
// enabled
|
|
21
|
+
this.buttonTarget.classList.add("bg-primary-600")
|
|
22
|
+
this.buttonTarget.classList.remove("bg-gray-200")
|
|
23
|
+
this.switchTarget.classList.add("translate-x-5")
|
|
24
|
+
this.switchTarget.classList.remove("translate-x-0")
|
|
25
|
+
|
|
26
|
+
if (this.hasCrossTarget && this.hasCheckTarget) {
|
|
27
|
+
this.crossTarget.classList.add("opacity-0", "ease-out", "duration-100")
|
|
28
|
+
this.checkTarget.classList.add("opacity-100", "ease-in", "duration-200")
|
|
29
|
+
|
|
30
|
+
this.crossTarget.classList.remove("opacity-100", "ease-in", "duration-200")
|
|
31
|
+
this.checkTarget.classList.remove("opacity-0", "ease-out", "duration-100")
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
// disabled
|
|
35
|
+
this.buttonTarget.classList.remove("bg-primary-600")
|
|
36
|
+
this.buttonTarget.classList.add("bg-gray-200")
|
|
37
|
+
this.switchTarget.classList.remove("translate-x-5")
|
|
38
|
+
this.switchTarget.classList.add("translate-x-0")
|
|
39
|
+
|
|
40
|
+
if (this.hasCrossTarget && this.hasCheckTarget) {
|
|
41
|
+
this.crossTarget.classList.remove("opacity-0", "ease-out", "duration-100")
|
|
42
|
+
this.checkTarget.classList.remove("opacity-100", "ease-in", "duration-200")
|
|
43
|
+
|
|
44
|
+
this.crossTarget.classList.add("opacity-100", "ease-in", "duration-200")
|
|
45
|
+
this.checkTarget.classList.add("opacity-0", "ease-out", "duration-100")
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Satis
|
|
4
|
+
module Tab
|
|
5
|
+
class Component < Satis::ApplicationComponent
|
|
6
|
+
attr_reader :options, :name, :icon, :badge
|
|
7
|
+
|
|
8
|
+
def initialize(name, *args, &block)
|
|
9
|
+
super
|
|
10
|
+
@name = name
|
|
11
|
+
@options = args.extract_options!
|
|
12
|
+
@args = args
|
|
13
|
+
@icon = options[:icon]
|
|
14
|
+
@badge = options[:badge]
|
|
15
|
+
@block = block
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def responsive?
|
|
19
|
+
options[:responsive] == true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def selected?
|
|
23
|
+
options[:selected] == true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def title
|
|
27
|
+
options[:title]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def call
|
|
31
|
+
content
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
.sts-tabs id="#{group}" data-controller="satis-tabs" data-satis-tabs-persist-value="#{persist}"
|
|
2
|
+
.sm:hidden
|
|
3
|
+
label.sr-only for="tabs" Select a tab
|
|
4
|
+
select#tabs.block.w-full.pl-3.pr-10.py-2.text-base.border-gray-300.focus:outline-none.focus:ring-primary-500.focus:border-primary-500.sm:text-sm.rounded-md name="tabs" data-action="change->satis-tabs#select" data-satis-tabs-target="select"
|
|
5
|
+
- tabs.each do |tab|
|
|
6
|
+
option selected=tab.selected? = ct(".#{tab.name}", scope: [group.to_sym], default: tab.name.to_s.humanize)
|
|
7
|
+
.hidden.sm:block
|
|
8
|
+
.border-b.border-gray-200
|
|
9
|
+
nav.-mb-px.flex.space-x-8.overflow-x-auto aria-label="Tabs"
|
|
10
|
+
- tabs.each do |tab|
|
|
11
|
+
a.tab id="#{tab.name}" href="#" aria-current="#{tab.selected? ? "page" : ''}" data-satis-tabs-target="tab" data-action="click->satis-tabs#select"
|
|
12
|
+
- if tab.icon
|
|
13
|
+
i.mr-2 class=tab.icon
|
|
14
|
+
= ct(".#{tab.name}", scope: [group.to_sym], default: tab.name.to_s.humanize)
|
|
15
|
+
i.fal.fa-triangle-exclamation.ml-2.hidden
|
|
16
|
+
- if tab.badge
|
|
17
|
+
span.badge
|
|
18
|
+
= tab.badge
|
|
19
|
+
|
|
20
|
+
div
|
|
21
|
+
- tabs.each do |tab|
|
|
22
|
+
div id="#{tab.name}-content" class="tab-content #{tab.options[:padding] == false ? '' : 'mt-4'}" data-satis-tabs-target="content"
|
|
23
|
+
= tab.to_s
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Tabs
|
|
2
|
+
|
|
3
|
+
## UI
|
|
4
|
+
|
|
5
|
+
https://tailwindui.com/components/application-ui/navigation/tabs
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```slim
|
|
10
|
+
= sts.tabs do |t|
|
|
11
|
+
- t.tab :about
|
|
12
|
+
p About
|
|
13
|
+
= link_to "Hello", root_path
|
|
14
|
+
- t.tab :printers
|
|
15
|
+
| printers
|
|
16
|
+
- t.tab :preferences
|
|
17
|
+
| preferences
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
satis.tabs takes an optional "group" parameter and is :main by default. So you could do:
|
|
21
|
+
`satis.tabs group: :second`
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Satis
|
|
4
|
+
module Tabs
|
|
5
|
+
class Component < Satis::ApplicationComponent
|
|
6
|
+
renders_many :tabs, Tab::Component
|
|
7
|
+
attr_reader :group, :persist
|
|
8
|
+
|
|
9
|
+
def initialize(group: :main, persist: false)
|
|
10
|
+
super
|
|
11
|
+
@group = group
|
|
12
|
+
@persist = persist
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
.sts-tabs {
|
|
2
|
+
.tab {
|
|
3
|
+
@apply border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap pb-4 px-1 border-b-2 font-medium text-sm border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300;
|
|
4
|
+
|
|
5
|
+
.badge {
|
|
6
|
+
@apply bg-gray-100 text-gray-600 hidden ml-3 rounded-full text-xs font-medium md:inline-block py-0.5 px-2.5;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
&.selected {
|
|
10
|
+
@apply border-primary-500 text-primary-600;
|
|
11
|
+
|
|
12
|
+
.badge {
|
|
13
|
+
@apply bg-primary-100 text-primary-600;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
&.is-invalid {
|
|
18
|
+
@apply text-red-600;
|
|
19
|
+
|
|
20
|
+
.fa-triangle-exclamation {
|
|
21
|
+
display: inline;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.tab-content {
|
|
27
|
+
@apply hidden;
|
|
28
|
+
|
|
29
|
+
&.selected {
|
|
30
|
+
@apply block;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import ApplicationController from "../../../../frontend/controllers/application_controller"
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Tabs controller
|
|
5
|
+
*/
|
|
6
|
+
export default class extends ApplicationController {
|
|
7
|
+
static targets = ["tab", "content", "select"]
|
|
8
|
+
static values = { persist: Boolean }
|
|
9
|
+
|
|
10
|
+
static keyBindings = [
|
|
11
|
+
{
|
|
12
|
+
keys: ["ctrl+1", "ctrl+2", "ctrl+3", "ctrl+4", "ctrl+5", "ctrl+6", "ctrl+7", "ctrl+8", "ctrl+9", "ctrl+0"],
|
|
13
|
+
handler: (event, combo, controller) => {
|
|
14
|
+
let index = -1 + +combo.split("+")[1]
|
|
15
|
+
if (index == -1) {
|
|
16
|
+
index = 10
|
|
17
|
+
}
|
|
18
|
+
controller.open(index)
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
connect() {
|
|
24
|
+
super.connect()
|
|
25
|
+
|
|
26
|
+
const ourUrl = new URL(window.location.href)
|
|
27
|
+
this.keyBase = ourUrl.pathname.substring(1, ourUrl.pathname.length).replace(/\//, "_") + "_tabs_" + this.context.scope.element.id
|
|
28
|
+
|
|
29
|
+
let firstErrorIndex
|
|
30
|
+
this.tabTargets.forEach((tab, index) => {
|
|
31
|
+
let hasErrors = this.contentTargets[index].querySelectorAll(".is-invalid")
|
|
32
|
+
if (hasErrors.length > 0) {
|
|
33
|
+
if (!firstErrorIndex) {
|
|
34
|
+
firstErrorIndex = index
|
|
35
|
+
}
|
|
36
|
+
tab.classList.add("is-invalid")
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
this.open(firstErrorIndex || this.tabToOpen())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
select(event) {
|
|
44
|
+
let index = null
|
|
45
|
+
if (event.srcElement.tagName == "SELECT") {
|
|
46
|
+
index = event.srcElement.selectedIndex
|
|
47
|
+
} else {
|
|
48
|
+
let clickedTab = event.srcElement.closest("a")
|
|
49
|
+
index = this.tabTargets.findIndex((el) => {
|
|
50
|
+
return el.attributes["id"] === clickedTab.attributes["id"]
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
this.open(index)
|
|
54
|
+
this.storeValue("openTab", index)
|
|
55
|
+
|
|
56
|
+
// Cancel the this event (dont show the browser context menu)
|
|
57
|
+
event.preventDefault()
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
open(index) {
|
|
62
|
+
if (index == -1 || this.tabTargets[index] === undefined) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.tabTargets.forEach(function (target) {
|
|
67
|
+
target.classList.remove("selected")
|
|
68
|
+
})
|
|
69
|
+
this.tabTargets[index].classList.add("selected")
|
|
70
|
+
|
|
71
|
+
this.contentTargets.forEach(function (target) {
|
|
72
|
+
target.classList.remove("selected")
|
|
73
|
+
})
|
|
74
|
+
this.contentTargets[index].classList.add("selected")
|
|
75
|
+
this.selectTarget.selectedIndex = index
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
storeValue(key, value) {
|
|
79
|
+
if (!this.persistValue) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (typeof Storage !== "undefined") {
|
|
84
|
+
sessionStorage.setItem(this.keyBase + "_" + key, value)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getValue(key) {
|
|
89
|
+
if (!this.persistValue) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (typeof Storage !== "undefined") {
|
|
94
|
+
return sessionStorage.getItem(this.keyBase + "_" + key)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
disconnect() {}
|
|
99
|
+
|
|
100
|
+
tabToOpen() {
|
|
101
|
+
let urlValue = this.getUrlVar(this.context.scope.element.id + "Tab")
|
|
102
|
+
|
|
103
|
+
if (typeof urlValue !== "undefined") {
|
|
104
|
+
return urlValue
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return this.getValue("openTab") || 0
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getUrlVar(name) {
|
|
111
|
+
return this.getUrlVars()[name]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
getUrlVars() {
|
|
115
|
+
let vars = {}
|
|
116
|
+
|
|
117
|
+
window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (m, key, value) {
|
|
118
|
+
vars[key] = value
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
return vars
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Satis
|
|
2
|
+
module ApplicationHelper
|
|
3
|
+
def sts
|
|
4
|
+
@_satis_helpers_container ||= Satis::Helpers::Container.new(self)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def method_missing(method, *args, **kwargs, &block)
|
|
8
|
+
if method.to_s.ends_with?('_url') || method.to_s.ends_with?('_path') && main_app.respond_to?(method)
|
|
9
|
+
main_app.send(method, *args, **kwargs, &block)
|
|
10
|
+
else
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
- if form.object.present? && form.object.errors.messages[collection].present?
|
|
2
|
+
div.invalid-feedback
|
|
3
|
+
= form.object.errors.full_messages_for(collection).join(', ')
|
|
4
|
+
|
|
5
|
+
- if template_object
|
|
6
|
+
template data-satis-fields-for-target='template'
|
|
7
|
+
= form.rails_fields_for collection, template_object, child_index: 'TEMPLATE' do |nested_form|
|
|
8
|
+
.nested-fields.template.py-2
|
|
9
|
+
= nested_form.input :id, as: :hidden
|
|
10
|
+
= nested_form.input :_destroy, as: :hidden
|
|
11
|
+
.grid.grid-cols-12.gap-4
|
|
12
|
+
.col-span-11.fields
|
|
13
|
+
= yield(nested_form)
|
|
14
|
+
.col-span-1.flex.justify-center.items-center.association
|
|
15
|
+
.h-full.w-1.border-r.border-dashed
|
|
16
|
+
a.text-primary-600.bg-white.dark:bg-gray-800 href="#" data-action='click->satis-fields-for#addAssociation' style="margin-left: -7px;"
|
|
17
|
+
i.fal.fa-plus
|
|
18
|
+
.hidden.col-span-1.flex.justify-center.items-center.association
|
|
19
|
+
.h-full.w-1.border-r.border-dashed
|
|
20
|
+
a.text-primary-600.bg-white.dark:bg-gray-800 href="#" data-action='click->satis-fields-for#removeAssociation' style="margin-left: -7px;"
|
|
21
|
+
i.fal.fa-trash
|
|
22
|
+
|
|
23
|
+
= form.rails_fields_for collection do |nested_form|
|
|
24
|
+
.nested-fields.py-2
|
|
25
|
+
= nested_form.input :id, as: :hidden
|
|
26
|
+
= nested_form.input :_destroy, as: :hidden
|
|
27
|
+
.grid.grid-cols-12.gap-4
|
|
28
|
+
.col-span-11
|
|
29
|
+
= yield(nested_form)
|
|
30
|
+
.col-span-1.flex.justify-center.items-center
|
|
31
|
+
.h-full.w-1.border-r.border-dashed
|
|
32
|
+
a.text-primary-600.bg-white.dark:bg-gray-800 href="#" data-action='click->satis-fields-for#removeAssociation' style="margin-left: -7px;"
|
|
33
|
+
i.fal.fa-trash
|
|
34
|
+
|
|
35
|
+
span data-satis-fields-for-target='insertionPoint'
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Satis
|
|
4
|
+
class StsWrapper
|
|
5
|
+
attr_reader :request
|
|
6
|
+
|
|
7
|
+
def initialize(request)
|
|
8
|
+
@request = request
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def browser
|
|
12
|
+
@browser ||= Browser.new(request.user_agent)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module ActionControllerHelpers
|
|
17
|
+
extend ActiveSupport::Concern
|
|
18
|
+
|
|
19
|
+
included do
|
|
20
|
+
def sts
|
|
21
|
+
StsWrapper.new(request)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class_methods do
|
|
26
|
+
# Nothing yet
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Satis
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :submit_on_enter, :confirm_before_leave
|
|
6
|
+
attr_writer :default_help_text
|
|
7
|
+
attr_writer :logger
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@logger = Logger.new(STDOUT)
|
|
11
|
+
@submit_on_enter = true
|
|
12
|
+
@confirm_before_leave = false
|
|
13
|
+
|
|
14
|
+
@default_help_text = lambda do |_template, _object, key, _additional_scope|
|
|
15
|
+
scope = help_scope(template, object, additional_scope)
|
|
16
|
+
|
|
17
|
+
value = I18n.t((["help"] + scope + [key.to_s]).join("."))
|
|
18
|
+
|
|
19
|
+
if /translation missing: (.+)/.match?(value)
|
|
20
|
+
nil
|
|
21
|
+
else
|
|
22
|
+
value
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Config: logger [Object].
|
|
28
|
+
def logger
|
|
29
|
+
@logger.is_a?(Proc) ? instance_exec(&@logger) : @logger
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def default_help_text(template, object, method, additional_scope)
|
|
33
|
+
if @default_help_text.is_a?(Proc)
|
|
34
|
+
instance_exec(template, object, method, additional_scope,
|
|
35
|
+
&@default_help_text)
|
|
36
|
+
else
|
|
37
|
+
@default_help_text
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Maybe not the right place?
|
|
42
|
+
def help_scope(template, object, additional_scope, action: nil)
|
|
43
|
+
scope = template.controller.controller_path.split("/")
|
|
44
|
+
scope << (action || template.controller.action_name)
|
|
45
|
+
scope << object.class.name.demodulize.tableize.singularize
|
|
46
|
+
|
|
47
|
+
scope += Array.wrap(additional_scope) if additional_scope
|
|
48
|
+
|
|
49
|
+
scope.map(&:to_s)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def help_scopes(template, object, additional_scope)
|
|
53
|
+
actions = [template.controller.action_name]
|
|
54
|
+
%w[show new edit create update destroy index].each do |action|
|
|
55
|
+
actions << action unless actions.include?(action)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
actions.map { |action| help_scope(template, object, additional_scope, action: action) }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/satis/engine.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require 'satis/forms/builder'
|
|
2
|
+
require 'satis/helpers/container'
|
|
3
|
+
require 'satis/menus/builder'
|
|
4
|
+
|
|
5
|
+
module Satis
|
|
6
|
+
class Engine < ::Rails::Engine
|
|
7
|
+
isolate_namespace Satis
|
|
8
|
+
|
|
9
|
+
config.autoload_paths << "#{root}/app/components"
|
|
10
|
+
config.autoload_paths << "#{root}/lib"
|
|
11
|
+
|
|
12
|
+
initializer 'satis.helper' do
|
|
13
|
+
Rails.application.reloader.to_prepare do
|
|
14
|
+
ActiveSupport.on_load :action_view do
|
|
15
|
+
include Satis::ApplicationHelper
|
|
16
|
+
|
|
17
|
+
# F*CK Rails, adding an extra surrounding div 'field_with_errors' breaking all the things.
|
|
18
|
+
self.field_error_proc = ->(html_tag, _instance) { html_tag }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
ActiveSupport.on_load(:action_controller) do
|
|
22
|
+
include Satis::ActionControllerHelpers
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|