studio-engine 0.96.2 → 0.97.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 +4 -4
- data/CHANGELOG.md +183 -0
- data/app/assets/tailwind/studio_engine/engine-motion.css +57 -0
- data/app/assets/tailwind/studio_engine/engine.css +8 -0
- data/app/javascript/studio/alpine_shims.js +23 -0
- data/app/javascript/studio/application.js +9 -21
- data/app/javascript/studio/board.js +593 -0
- data/app/javascript/studio/booking.js +176 -0
- data/app/javascript/studio/controllers/board_controller.js +41 -0
- data/app/javascript/studio/controllers/booking_controller.js +24 -0
- data/app/javascript/studio/controllers/email_banner_scale_controller.js +22 -0
- data/app/javascript/studio/controllers/footer_map_controller.js +22 -0
- data/app/javascript/studio/controllers/geo_settings_controller.js +24 -0
- data/app/javascript/studio/controllers/hold_button_controller.js +64 -0
- data/app/javascript/studio/controllers/link_preview_card_controller.js +25 -0
- data/app/javascript/studio/controllers/link_sidebar_controller.js +18 -0
- data/app/javascript/studio/controllers/survey_controller.js +18 -0
- data/app/javascript/studio/controllers/toast_controller.js +26 -0
- data/app/javascript/studio/email_banner.js +277 -0
- data/app/javascript/studio/email_banner_scale.js +60 -0
- data/app/javascript/studio/footer_map.js +143 -0
- data/app/javascript/studio/geo_settings.js +105 -0
- data/app/javascript/studio/hold_button.js +416 -0
- data/app/javascript/studio/hold_button_hooks.js +162 -0
- data/app/javascript/studio/lazy_controllers.js +138 -0
- data/app/javascript/studio/leveling_activity.js +252 -0
- data/app/javascript/studio/link_preview_card.js +34 -0
- data/app/javascript/studio/link_sidebar.js +157 -0
- data/app/javascript/studio/stimulus.js +72 -0
- data/app/javascript/studio/survey.js +316 -0
- data/app/javascript/studio/theme_editor.js +197 -0
- data/app/javascript/studio/toast.js +270 -0
- data/app/views/components/_link_sidebar.html.erb +5 -100
- data/app/views/components/_link_sidebar_trigger.html.erb +2 -2
- data/app/views/layouts/studio/_flash.html.erb +13 -118
- data/app/views/layouts/studio/_head.html.erb +30 -14
- data/app/views/studio/_board_assets.html.erb +9 -452
- data/app/views/studio/_hold_button.html.erb +38 -445
- data/app/views/studio/_hold_button_guard.html.erb +172 -0
- data/app/views/studio/_leveling_activity_assets.html.erb +9 -283
- data/app/views/studio/board/_board.html.erb +16 -5
- data/app/views/studio/booking/_assets.html.erb +5 -139
- data/app/views/studio/booking/_frame.html.erb +3 -3
- data/app/views/studio/booking/_popup.html.erb +4 -4
- data/app/views/studio/emails/_banner_editor.html.erb +4 -152
- data/app/views/studio/emails/_banner_scale.html.erb +5 -56
- data/app/views/studio/emails/index.html.erb +0 -1
- data/app/views/studio/geo_settings/edit.html.erb +1 -97
- data/app/views/studio/site_footer/_map.html.erb +7 -5
- data/app/views/studio/site_footer/_map_assets.html.erb +5 -129
- data/app/views/studio/site_identities/edit.html.erb +1 -24
- data/app/views/studio/surveys/show.html.erb +4 -5
- data/app/views/style/_tasks.html.erb +3 -8
- data/app/views/style/_theme.html.erb +2 -52
- data/app/views/theme_settings/edit.html.erb +3 -82
- data/config/importmap.rb +6 -0
- data/docs/BOOKING.md +6 -5
- data/docs/SITE_FOOTER.md +16 -12
- data/lib/studio/version.rb +1 -1
- metadata +28 -3
- data/app/views/studio/emails/_recipient_repaint.html.erb +0 -122
- data/app/views/studio/surveys/_script.html.erb +0 -309
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// studio/booking: the booking frame and the booking popup (docs/BOOKING.md).
|
|
2
|
+
//
|
|
3
|
+
// installBookingFrames the inline frame: assign src after `load`, once the
|
|
4
|
+
// frame is near the viewport, and open the crop when
|
|
5
|
+
// focus moves into the frame.
|
|
6
|
+
// installBookingPopup any a[data-booking-popup] link opens the booking
|
|
7
|
+
// dialog instead of leaving the page.
|
|
8
|
+
//
|
|
9
|
+
// Both install document-level behaviour once per document, however many
|
|
10
|
+
// frames, dialogs or Turbo visits follow. Started by the booking controller,
|
|
11
|
+
// which studio/booking/_frame and studio/booking/_popup name.
|
|
12
|
+
//
|
|
13
|
+
// THE GUARDS ARE THE ENGINE'S OWN NAMES, AND THE ELEMENTS ARE THE ENGINE'S OWN
|
|
14
|
+
// (data-studio-booking), for the reason studio/footer_map gives for the map: an
|
|
15
|
+
// app that still renders a local booking script guards it with the unprefixed
|
|
16
|
+
// names and marks its frame, dialog and links with the same data-booking-*
|
|
17
|
+
// attributes. Sharing either lets one script stand the other down across a
|
|
18
|
+
// Turbo visit, or act on elements that are not its own.
|
|
19
|
+
//
|
|
20
|
+
// It imports nothing; test/javascript/booking.test.mjs loads it as a data:
|
|
21
|
+
// module.
|
|
22
|
+
|
|
23
|
+
export const FRAME = "iframe[data-booking-frame][data-studio-booking]"
|
|
24
|
+
export const WAITING_FRAMES = "iframe[data-booking-frame][data-studio-booking][data-src]"
|
|
25
|
+
export const WRAP = "[data-booking-wrap][data-studio-booking]"
|
|
26
|
+
export const SHUT_WRAP = "[data-booking-wrap][data-studio-booking][data-booking-crop]:not(.is-open)"
|
|
27
|
+
export const POPUP_LINK = "a[data-booking-popup][data-studio-booking]"
|
|
28
|
+
export const DIALOG = "dialog[data-booking-dialog][data-studio-booking]"
|
|
29
|
+
export const POPUP_FRAME = "iframe[data-booking-popup-frame]"
|
|
30
|
+
|
|
31
|
+
// ASK GOOGLE ONCE. Two things can ask for a frame: scrolling near it, and a
|
|
32
|
+
// booking link that jumps to it. Assigning src a second time, even to the same
|
|
33
|
+
// URL, is a second request, so whoever comes second finds it set.
|
|
34
|
+
export function loadBookingFrame(frame) {
|
|
35
|
+
if (!frame.getAttribute("src")) frame.src = frame.dataset.src
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// A plain primary click: not already handled, and not one the visitor modified
|
|
39
|
+
// to open a new tab or window.
|
|
40
|
+
export function plainClick(event) {
|
|
41
|
+
return !(event.defaultPrevented || event.button !== 0 ||
|
|
42
|
+
event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Watches every waiting frame in `doc`, and loads each once it comes within
|
|
46
|
+
// 200px of the viewport. A frame is watched once.
|
|
47
|
+
export function armBookingFrames(win, doc) {
|
|
48
|
+
doc.querySelectorAll(WAITING_FRAMES).forEach((frame) => {
|
|
49
|
+
if (frame.__bookingArmed) return
|
|
50
|
+
frame.__bookingArmed = true
|
|
51
|
+
if (typeof win.IntersectionObserver !== "function") { loadBookingFrame(frame); return }
|
|
52
|
+
const watcher = new win.IntersectionObserver((entries) => {
|
|
53
|
+
if (!entries.some((entry) => entry.isIntersecting)) return
|
|
54
|
+
watcher.disconnect()
|
|
55
|
+
loadBookingFrame(frame)
|
|
56
|
+
}, { rootMargin: "200px" })
|
|
57
|
+
watcher.observe(frame)
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Arms the frames once the window has loaded: a frame that starts loading
|
|
62
|
+
// before `load` holds that event open for as long as Google takes to answer.
|
|
63
|
+
export function armBookingFramesAfterLoad(win, doc) {
|
|
64
|
+
if (doc.readyState === "complete") armBookingFrames(win, doc)
|
|
65
|
+
else win.addEventListener("load", () => armBookingFrames(win, doc), { once: true })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Focus moving into the frame is the one signal a cross-origin frame gives its
|
|
69
|
+
// parent. It means the visitor has started using the calendar.
|
|
70
|
+
function openActive(doc) {
|
|
71
|
+
const active = doc.activeElement
|
|
72
|
+
if (!active || !active.matches || !active.matches(FRAME)) return
|
|
73
|
+
const wrap = active.closest(WRAP)
|
|
74
|
+
if (wrap) wrap.classList.add("is-open")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Installs the inline frame's behaviour, once per document. Answers false when
|
|
78
|
+
// it was already installed.
|
|
79
|
+
export function installBookingFrames(win, doc) {
|
|
80
|
+
if (win.__studioBookingFramesArmed) return false
|
|
81
|
+
win.__studioBookingFramesArmed = true
|
|
82
|
+
win.__studioBookingLoad = loadBookingFrame
|
|
83
|
+
|
|
84
|
+
// THE WINDOW BLURS ONCE. Focus that moves from one frame straight into
|
|
85
|
+
// another never comes back through this window, so no second blur announces
|
|
86
|
+
// the second frame, and on a page with two cropped frames it would stay
|
|
87
|
+
// cropped under the visitor's cursor. So while focus is away in a frame and a
|
|
88
|
+
// cropped wrapper is still shut, look again a few times a second. It stops
|
|
89
|
+
// when focus returns here or nothing is left to open, and a page with one
|
|
90
|
+
// frame never starts it: that frame is open by then.
|
|
91
|
+
let watching = null
|
|
92
|
+
const shut = () => doc.querySelector(SHUT_WRAP)
|
|
93
|
+
const stopWatching = () => { if (watching) { win.clearInterval(watching); watching = null } }
|
|
94
|
+
|
|
95
|
+
win.addEventListener("blur", () => {
|
|
96
|
+
openActive(doc)
|
|
97
|
+
const active = doc.activeElement
|
|
98
|
+
if (watching || !active || active.tagName !== "IFRAME" || !shut()) return
|
|
99
|
+
watching = win.setInterval(() => {
|
|
100
|
+
openActive(doc)
|
|
101
|
+
if (!shut()) stopWatching()
|
|
102
|
+
}, 250)
|
|
103
|
+
})
|
|
104
|
+
win.addEventListener("focus", stopWatching)
|
|
105
|
+
|
|
106
|
+
doc.addEventListener("turbo:load", () => armBookingFramesAfterLoad(win, doc))
|
|
107
|
+
armBookingFramesAfterLoad(win, doc)
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// What a click on a booking link does. Delegated from the document, so it
|
|
112
|
+
// survives Turbo body swaps. A modified click (new tab, new window) is left
|
|
113
|
+
// alone, and so is everything when the dialog is missing or <dialog> is
|
|
114
|
+
// unsupported: the link's own href then takes the visitor to the booking page.
|
|
115
|
+
//
|
|
116
|
+
// ON A PAGE THAT ALREADY SHOWS THE INLINE FRAME the link goes to that frame
|
|
117
|
+
// instead: it scrolls there, opens the crop and moves focus in. A popup would
|
|
118
|
+
// be a second copy of the calendar on top of the first.
|
|
119
|
+
//
|
|
120
|
+
// Answers "frame", "dialog", or null when the click is left to the browser.
|
|
121
|
+
export function handleBookingLinkClick(event, win, doc) {
|
|
122
|
+
const link = event.target.closest && event.target.closest(POPUP_LINK)
|
|
123
|
+
if (!link) return null
|
|
124
|
+
if (!plainClick(event)) return null
|
|
125
|
+
|
|
126
|
+
const inline = doc.querySelector(FRAME)
|
|
127
|
+
if (inline) {
|
|
128
|
+
event.preventDefault()
|
|
129
|
+
const wrap = inline.closest(WRAP)
|
|
130
|
+
if (wrap) wrap.classList.add("is-open")
|
|
131
|
+
// Before `load` the frame keeps waiting: installBookingFrames assigns src then.
|
|
132
|
+
if (doc.readyState === "complete" && win.__studioBookingLoad) win.__studioBookingLoad(inline)
|
|
133
|
+
const calm = win.matchMedia && win.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
134
|
+
const anchor = wrap || inline
|
|
135
|
+
anchor.scrollIntoView({ behavior: calm ? "auto" : "smooth", block: "start" })
|
|
136
|
+
inline.focus({ preventScroll: true })
|
|
137
|
+
return "frame"
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const dialog = doc.querySelector(DIALOG)
|
|
141
|
+
if (!dialog || typeof dialog.showModal !== "function") return null
|
|
142
|
+
|
|
143
|
+
event.preventDefault()
|
|
144
|
+
const frame = dialog.querySelector(POPUP_FRAME)
|
|
145
|
+
if (frame && !frame.getAttribute("src")) frame.src = frame.dataset.src
|
|
146
|
+
if (!dialog.open) dialog.showModal()
|
|
147
|
+
return "dialog"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Never snapshot the dialog open: a restored page would show it stuck. And
|
|
151
|
+
// never snapshot its frame loaded: a restored page would ask Google for a popup
|
|
152
|
+
// nobody has opened. The src goes back to waiting in data-src.
|
|
153
|
+
export function resetBookingDialog(doc) {
|
|
154
|
+
const dialog = doc.querySelector(DIALOG)
|
|
155
|
+
if (!dialog) return
|
|
156
|
+
if (dialog.open) dialog.close()
|
|
157
|
+
const frame = dialog.querySelector(POPUP_FRAME)
|
|
158
|
+
if (frame) frame.removeAttribute("src")
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Installs the popup's behaviour, once per document. Answers false when it was
|
|
162
|
+
// already installed.
|
|
163
|
+
export function installBookingPopup(win, doc) {
|
|
164
|
+
if (win.__studioBookingPopupArmed) return false
|
|
165
|
+
win.__studioBookingPopupArmed = true
|
|
166
|
+
|
|
167
|
+
doc.addEventListener("click", (event) => { handleBookingLinkClick(event, win, doc) })
|
|
168
|
+
|
|
169
|
+
// A click on the backdrop lands on the dialog element itself.
|
|
170
|
+
doc.addEventListener("click", (event) => {
|
|
171
|
+
if (event.target.matches && event.target.matches(DIALOG)) event.target.close()
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
doc.addEventListener("turbo:before-cache", () => resetBookingDialog(doc))
|
|
175
|
+
return true
|
|
176
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// board: one board primitive (studio/board/_board), on the engine's Stimulus
|
|
2
|
+
// application, which reads data-studio-controller and registers this controller
|
|
3
|
+
// lazily, the first time a page renders a board. It sits on the element that
|
|
4
|
+
// carries the board's Alpine scope:
|
|
5
|
+
//
|
|
6
|
+
// <section x-data="studioBoard({...})" data-studio-controller="board">
|
|
7
|
+
//
|
|
8
|
+
// The scope (studio/board) owns the state, the requests and the toasts. This
|
|
9
|
+
// controller owns what needs the page: it loads SortableJS, so only a page with
|
|
10
|
+
// a board fetches it, and wires the zones when both the library and the scope
|
|
11
|
+
// are there. Until it connects the board renders and its chrome works; a card
|
|
12
|
+
// does not drag, and the element carries no data-alpine-ready.
|
|
13
|
+
import { Controller } from "@hotwired/stimulus"
|
|
14
|
+
import { loadSortable, scopeFor } from "studio/board"
|
|
15
|
+
|
|
16
|
+
export default class extends Controller {
|
|
17
|
+
connect() {
|
|
18
|
+
const connection = this.connection = {}
|
|
19
|
+
// A board whose SortableJS fails to load is wired without it: it still
|
|
20
|
+
// counts, toasts and follows live updates.
|
|
21
|
+
const sortable = loadSortable().catch((error) => {
|
|
22
|
+
console.error("[studio] SortableJS failed to load; the board does not drag", error)
|
|
23
|
+
return null
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
Promise.all([scopeFor(this.element), sortable]).then(([scope, Sortable]) => {
|
|
27
|
+
if (this.connection !== connection) return
|
|
28
|
+
connection.unwire = scope.wire(Sortable)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
disconnect() {
|
|
33
|
+
this.unwire()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
unwire() {
|
|
37
|
+
const connection = this.connection
|
|
38
|
+
this.connection = null
|
|
39
|
+
if (connection && connection.unwire) connection.unwire()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// booking: the booking frame and the booking popup, on the engine's Stimulus
|
|
2
|
+
// application (studio/stimulus, which reads data-studio-controller and
|
|
3
|
+
// registers this controller lazily). It sits on the frame's wrapper
|
|
4
|
+
// (studio/booking/_frame) and on the popup's dialog (studio/booking/_popup):
|
|
5
|
+
//
|
|
6
|
+
// <div class="booking-frame" data-booking-wrap data-studio-booking
|
|
7
|
+
// data-studio-controller="booking">
|
|
8
|
+
// <dialog class="booking-popup" data-booking-dialog data-studio-booking
|
|
9
|
+
// data-studio-controller="booking">
|
|
10
|
+
//
|
|
11
|
+
// The behaviour is studio/booking, installed once per document however many of
|
|
12
|
+
// these connect. Until it is installed a booking link is an ordinary link to
|
|
13
|
+
// the booking page.
|
|
14
|
+
import { Controller } from "@hotwired/stimulus"
|
|
15
|
+
import { installBookingFrames, installBookingPopup, armBookingFramesAfterLoad } from "studio/booking"
|
|
16
|
+
|
|
17
|
+
export default class extends Controller {
|
|
18
|
+
connect() {
|
|
19
|
+
const first = installBookingFrames(window, document)
|
|
20
|
+
installBookingPopup(window, document)
|
|
21
|
+
// A frame that arrives after the install (a stream, a second wrapper).
|
|
22
|
+
if (!first) armBookingFramesAfterLoad(window, document)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// email-banner-scale: fits every email banner preview on the page to its frame,
|
|
2
|
+
// on the engine's Stimulus application (studio/stimulus, which reads
|
|
3
|
+
// data-studio-controller and registers this controller lazily). It sits on the
|
|
4
|
+
// anchor studio/emails/_banner_scale renders, once per page:
|
|
5
|
+
//
|
|
6
|
+
// <template data-studio-controller="email-banner-scale"></template>
|
|
7
|
+
//
|
|
8
|
+
// The fit is studio/email_banner_scale. Connecting again after a Turbo visit
|
|
9
|
+
// fits the new body's previews; disconnecting drops the observer.
|
|
10
|
+
import { Controller } from "@hotwired/stimulus"
|
|
11
|
+
import { startBannerScale } from "studio/email_banner_scale"
|
|
12
|
+
|
|
13
|
+
export default class extends Controller {
|
|
14
|
+
connect() {
|
|
15
|
+
this.stop = startBannerScale(document, window.ResizeObserver)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
disconnect() {
|
|
19
|
+
if (this.stop) this.stop()
|
|
20
|
+
this.stop = null
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// footer-map: one live map of an address, on the engine's Stimulus application
|
|
2
|
+
// (studio/stimulus, which reads data-studio-controller and registers this
|
|
3
|
+
// controller lazily). It sits on the map element studio/site_footer/_map
|
|
4
|
+
// renders:
|
|
5
|
+
//
|
|
6
|
+
// <div class="ftr-map" data-footer-map data-studio-controller="footer-map"
|
|
7
|
+
// data-lat="..." data-lng="..." data-leaflet-js="..." data-leaflet-css="...">
|
|
8
|
+
//
|
|
9
|
+
// The mount is studio/footer_map, installed once per document however many maps
|
|
10
|
+
// connect. Until Leaflet mounts, and if it never does, the link inside the
|
|
11
|
+
// element is the whole map.
|
|
12
|
+
import { Controller } from "@hotwired/stimulus"
|
|
13
|
+
import { installFooterMaps } from "studio/footer_map"
|
|
14
|
+
|
|
15
|
+
export default class extends Controller {
|
|
16
|
+
connect() {
|
|
17
|
+
const installed = !window.__studioFooterMapsArmed
|
|
18
|
+
const arm = installFooterMaps(window, document)
|
|
19
|
+
// A map that arrives after the install (a stream, a second map).
|
|
20
|
+
if (!installed) arm()
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// geo-settings: the geo manager's live preview, on the engine's Stimulus
|
|
2
|
+
// application (studio/stimulus, which reads data-studio-controller and
|
|
3
|
+
// registers this controller lazily). It sits on the page's root:
|
|
4
|
+
//
|
|
5
|
+
// <div data-geo-page data-studio-controller="geo-settings"
|
|
6
|
+
// data-geo-home="US" data-geo-country="US" data-geo-subdivision="CO"
|
|
7
|
+
// data-geo-fail-closed="true">
|
|
8
|
+
//
|
|
9
|
+
// The rules and the painting are studio/geo_settings. Any change inside the
|
|
10
|
+
// page repaints.
|
|
11
|
+
import { Controller } from "@hotwired/stimulus"
|
|
12
|
+
import { paintGeo } from "studio/geo_settings"
|
|
13
|
+
|
|
14
|
+
export default class extends Controller {
|
|
15
|
+
connect() {
|
|
16
|
+
this.paint = () => paintGeo(this.element, document)
|
|
17
|
+
this.element.addEventListener("change", this.paint)
|
|
18
|
+
this.paint()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
disconnect() {
|
|
22
|
+
this.element.removeEventListener("change", this.paint)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// hold-button: one hold-to-confirm button, on the engine's Stimulus application
|
|
2
|
+
// (studio/stimulus, which reads data-studio-controller and imports this
|
|
3
|
+
// controller statically, so it arrives with the page). It sits on the
|
|
4
|
+
// stack studio/_hold_button renders, and the button carries the presses:
|
|
5
|
+
//
|
|
6
|
+
// <span class="hold-stack" data-studio-controller="hold-button">
|
|
7
|
+
// <button class="hold-btn" data-hold-id="confirm" data-duration="2000"
|
|
8
|
+
// data-studio-action="mousedown->hold-button#start mouseup->hold-button#end
|
|
9
|
+
// mouseleave->hold-button#end touchstart->hold-button#press
|
|
10
|
+
// touchend->hold-button#end touchcancel->hold-button#end">
|
|
11
|
+
//
|
|
12
|
+
// The timeline, the idle nudge and the fizz portal are studio/hold_button; the
|
|
13
|
+
// events and string locals the timeline asks are studio/hold_button_hooks.
|
|
14
|
+
import { Controller } from "@hotwired/stimulus"
|
|
15
|
+
import { Hold, mountPortal } from "studio/hold_button"
|
|
16
|
+
import { holdHooks } from "studio/hold_button_hooks"
|
|
17
|
+
|
|
18
|
+
const NUDGES = ["hold-nudge", "hold-nudge-soft"]
|
|
19
|
+
|
|
20
|
+
// What a connected stack carries. The guard in the head
|
|
21
|
+
// (studio/_hold_button_guard) marks a stack that never gets it as unavailable,
|
|
22
|
+
// and takes that mark back here when the controller connects late.
|
|
23
|
+
const CONNECTED = "data-hold-button-connected"
|
|
24
|
+
|
|
25
|
+
export default class extends Controller {
|
|
26
|
+
connect() {
|
|
27
|
+
this.element.setAttribute(CONNECTED, "")
|
|
28
|
+
if (window.studioHoldButtonGuard) window.studioHoldButtonGuard.clear(this.element)
|
|
29
|
+
|
|
30
|
+
this.button = this.element.querySelector(":scope > .hold-btn")
|
|
31
|
+
if (!this.button) return
|
|
32
|
+
|
|
33
|
+
this.hold = new Hold(this.button, holdHooks(this.button, { win: window }))
|
|
34
|
+
this.clearNudge = (event) => {
|
|
35
|
+
if (NUDGES.includes(event.animationName)) this.button.classList.remove("nudge", "nudge-soft")
|
|
36
|
+
}
|
|
37
|
+
this.button.addEventListener("animationend", this.clearNudge)
|
|
38
|
+
this.hold.nudge.start()
|
|
39
|
+
if (this.element.hasAttribute("data-fizz-portal")) mountPortal(this.element)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
disconnect() {
|
|
43
|
+
this.element.removeAttribute(CONNECTED)
|
|
44
|
+
if (!this.hold) return
|
|
45
|
+
this.hold.stop()
|
|
46
|
+
this.hold = null
|
|
47
|
+
this.button.removeEventListener("animationend", this.clearNudge)
|
|
48
|
+
// The portal notices its stack leaving by itself (studio/hold_button).
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
start() {
|
|
52
|
+
if (this.hold) this.hold.start()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// A touch: no synthetic mouse events, no long-press menu.
|
|
56
|
+
press(event) {
|
|
57
|
+
event.preventDefault()
|
|
58
|
+
this.start()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
end() {
|
|
62
|
+
if (this.hold) this.hold.end()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// link-preview-card: the site identity manager's live card, on the engine's
|
|
2
|
+
// Stimulus application (studio/stimulus, which reads data-studio-controller and
|
|
3
|
+
// registers this controller lazily). It sits on the page's root:
|
|
4
|
+
//
|
|
5
|
+
// <div data-link-preview-page data-studio-controller="link-preview-card"
|
|
6
|
+
// data-fallback-title="..." data-fallback-description="...">
|
|
7
|
+
//
|
|
8
|
+
// The repaint is studio/link_preview_card. One listener on the root takes the
|
|
9
|
+
// `input` of every [data-link-preview-input] inside it.
|
|
10
|
+
import { Controller } from "@hotwired/stimulus"
|
|
11
|
+
import { paintCard } from "studio/link_preview_card"
|
|
12
|
+
|
|
13
|
+
export default class extends Controller {
|
|
14
|
+
connect() {
|
|
15
|
+
this.repaint = (event) => {
|
|
16
|
+
const input = event.target
|
|
17
|
+
if (input && input.matches && input.matches("[data-link-preview-input]")) paintCard(this.element, input)
|
|
18
|
+
}
|
|
19
|
+
this.element.addEventListener("input", this.repaint)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
disconnect() {
|
|
23
|
+
this.element.removeEventListener("input", this.repaint)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// link-sidebar: the link sidebar's presence on the page, on the engine's
|
|
2
|
+
// Stimulus application (studio/application, which reads data-studio-controller).
|
|
3
|
+
// It sits on the anchor components/_link_sidebar renders beside its panels:
|
|
4
|
+
//
|
|
5
|
+
// <template data-studio-controller="link-sidebar"></template>
|
|
6
|
+
//
|
|
7
|
+
// The open flag and the document handlers are studio/link_sidebar, which
|
|
8
|
+
// registers $store.sidebars.linkTreeOpen on alpine:init and before a Turbo
|
|
9
|
+
// render. connect() registers it too, for a sidebar that arrived any other way.
|
|
10
|
+
// Registered by studio/application.
|
|
11
|
+
import { Controller } from "@hotwired/stimulus"
|
|
12
|
+
import { ensureLinkSidebarStore } from "studio/link_sidebar"
|
|
13
|
+
|
|
14
|
+
export default class extends Controller {
|
|
15
|
+
connect() {
|
|
16
|
+
ensureLinkSidebarStore(window.Alpine)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// survey: the survey stepper, on the engine's Stimulus application
|
|
2
|
+
// (studio/stimulus, which reads data-studio-controller and registers this
|
|
3
|
+
// controller lazily). It sits on the survey's root (studio/surveys/show):
|
|
4
|
+
//
|
|
5
|
+
// <main class="studio-survey" data-studio-survey data-studio-controller="survey"
|
|
6
|
+
// data-answer-base="..." data-resume-index="..." data-csrf="...">
|
|
7
|
+
//
|
|
8
|
+
// The stepper is studio/survey, which binds a root once. A page restored from
|
|
9
|
+
// Turbo's cache is a new element, so it connects and is bound again. Until the
|
|
10
|
+
// controller connects, and if it never does, the page is the plain form.
|
|
11
|
+
import { Controller } from "@hotwired/stimulus"
|
|
12
|
+
import { enhanceSurvey } from "studio/survey"
|
|
13
|
+
|
|
14
|
+
export default class extends Controller {
|
|
15
|
+
connect() {
|
|
16
|
+
enhanceSurvey(this.element, window, document)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// toast: the toast root's lifetime, on the engine's Stimulus application
|
|
2
|
+
// (studio/application, which reads data-studio-controller). It sits on the
|
|
3
|
+
// root of layouts/studio/_flash:
|
|
4
|
+
//
|
|
5
|
+
// <div x-data data-studio-controller="toast"
|
|
6
|
+
// data-toast-initial-value='[{"type":"notice","message":"Saved"}]'>
|
|
7
|
+
//
|
|
8
|
+
// The queue is studio/toast ($store.toasts), which also seeds a rendered root
|
|
9
|
+
// on a full load and on turbo:load. connect() seeds this root too, for markup
|
|
10
|
+
// that arrived any other way; an element is seeded once. disconnect() empties
|
|
11
|
+
// the queue when the root leaves the page with no other root left to show it.
|
|
12
|
+
// Registered by studio/application.
|
|
13
|
+
import { Controller } from "@hotwired/stimulus"
|
|
14
|
+
import { seedToasts, TOAST_ROOT } from "studio/toast"
|
|
15
|
+
|
|
16
|
+
export default class extends Controller {
|
|
17
|
+
connect() {
|
|
18
|
+
seedToasts(this.element, { win: window })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
disconnect() {
|
|
22
|
+
if (document.querySelector(TOAST_ROOT)) return
|
|
23
|
+
const store = window.Alpine && window.Alpine.store("toasts")
|
|
24
|
+
if (store && typeof store.clear === "function") store.clear()
|
|
25
|
+
}
|
|
26
|
+
}
|