studio-engine 0.97.0 → 0.98.0.rc1
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 +89 -0
- data/app/javascript/studio/alpine_scopes.js +66 -0
- data/app/javascript/studio/alpine_shims.js +4 -0
- data/app/javascript/studio/application.js +4 -1
- data/app/javascript/studio/birthday.js +351 -0
- data/app/javascript/studio/confirm_interstitial.js +44 -0
- data/app/javascript/studio/controllers/identity_mini_controller.js +32 -0
- data/app/javascript/studio/controllers/style_modals_controller.js +41 -0
- data/app/javascript/studio/cropper.js +281 -0
- data/app/javascript/studio/identity_mini.js +74 -0
- data/app/javascript/studio/image_upload.js +189 -0
- data/app/javascript/studio/profile_form.js +88 -0
- data/app/javascript/studio/stimulus.js +4 -0
- data/app/javascript/studio/style_guide.js +13 -0
- data/app/javascript/studio/style_modals.js +214 -0
- data/app/views/components/_avatar_cropper.html.erb +3 -38
- data/app/views/layouts/studio/_head.html.erb +12 -2
- data/app/views/studio/_alpine_scopes_guard.html.erb +178 -0
- data/app/views/studio/_birthday_assets.html.erb +6 -240
- data/app/views/studio/_confirm_interstitial.html.erb +31 -18
- data/app/views/studio/_cropper_assets.html.erb +9 -8
- data/app/views/studio/modals/_crop_photo.html.erb +1 -1
- data/app/views/studio/modals/_image_upload.html.erb +8 -321
- data/app/views/studio/modals/blocks/_birthday.html.erb +2 -3
- data/app/views/studio/profiles/_birthday_fields.html.erb +0 -1
- data/app/views/studio/profiles/_identity_mini.html.erb +5 -62
- data/app/views/studio/profiles/_plain_save.html.erb +11 -0
- data/app/views/studio/profiles/edit.html.erb +1 -6
- data/app/views/style/_modals.html.erb +36 -420
- data/lib/studio/version.rb +1 -1
- metadata +14 -3
- data/app/views/studio/profiles/_birthday_picker_script.html.erb +0 -146
- data/app/views/studio/profiles/_form_script.html.erb +0 -65
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
<%# The birthday field's Alpine component.
|
|
2
|
-
|
|
3
|
-
Supplies exactly what studio/fields/_date_of_birth binds to — month, day,
|
|
4
|
-
year, months, dayOptions, years, onMonthChange() — plus the one thing the
|
|
5
|
-
profile page needs on top: it writes the assembled ISO date into the
|
|
6
|
-
enclosing studioProfileForm's `fields.birthday`, which is what raises the
|
|
7
|
-
save bar.
|
|
8
|
-
|
|
9
|
-
THIS REPLACED A CALENDAR POPOVER, and the replacement is the point. That
|
|
10
|
-
component shipped four defects in three days:
|
|
11
|
-
|
|
12
|
-
· its day grid used `grid-cols-7`, which exists in NO consumer bundle, so
|
|
13
|
-
the seven weekday letters stacked into one column and the popover grew to
|
|
14
|
-
the height of the page;
|
|
15
|
-
· a flipped popover positioned itself from a hardcoded 340px height when
|
|
16
|
-
the real popover is ~245px, landing ~95px too high;
|
|
17
|
-
· place() ran on every scroll and RE-DECIDED the side, so it snapped
|
|
18
|
-
between above and below mid-scroll — a 254px jump between two 60px steps;
|
|
19
|
-
· and it came back OPEN after a browser Back.
|
|
20
|
-
|
|
21
|
-
Three selects have none of those failure modes. There is no position, no
|
|
22
|
-
flip, no popover, and no scroll listener — so there is nothing to place
|
|
23
|
-
wrongly, nothing to re-decide, and nothing to restore open.
|
|
24
|
-
%>
|
|
25
|
-
<script>
|
|
26
|
-
window.studioBirthdayFields = function (initial) {
|
|
27
|
-
var parts = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(initial || ""));
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
year: parts ? String(+parts[1]) : "",
|
|
31
|
-
month: parts ? String(+parts[2]) : "",
|
|
32
|
-
day: parts ? String(+parts[3]) : "",
|
|
33
|
-
|
|
34
|
-
months: [
|
|
35
|
-
{ n: 1, label: "January" }, { n: 2, label: "February" },
|
|
36
|
-
{ n: 3, label: "March" }, { n: 4, label: "April" },
|
|
37
|
-
{ n: 5, label: "May" }, { n: 6, label: "June" },
|
|
38
|
-
{ n: 7, label: "July" }, { n: 8, label: "August" },
|
|
39
|
-
{ n: 9, label: "September" }, { n: 10, label: "October" },
|
|
40
|
-
{ n: 11, label: "November" }, { n: 12, label: "December" }
|
|
41
|
-
],
|
|
42
|
-
|
|
43
|
-
// Oldest year offered. 120 covers every living person with room to spare;
|
|
44
|
-
// the newest is THIS year, because a birthday cannot be in the future and
|
|
45
|
-
// the cheapest way to say so is not to offer one.
|
|
46
|
-
get years() {
|
|
47
|
-
var now = new Date().getFullYear();
|
|
48
|
-
var out = [];
|
|
49
|
-
for (var y = now; y >= now - 120; y--) out.push(y);
|
|
50
|
-
return out;
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
// The days that exist in the chosen month — 30 in April, and 29 in a
|
|
54
|
-
// February only when the chosen year is a leap year. Day 0 of the NEXT
|
|
55
|
-
// month is the last day of this one, which is the standard trick and
|
|
56
|
-
// handles the leap rule without one being written down.
|
|
57
|
-
get dayOptions() {
|
|
58
|
-
var m = parseInt(this.month, 10);
|
|
59
|
-
var y = parseInt(this.year, 10) || 2000; // a leap year, so Feb offers 29 before a year is picked
|
|
60
|
-
if (!m) return Array.from({ length: 31 }, function (_, i) { return i + 1; });
|
|
61
|
-
|
|
62
|
-
var count = new Date(y, m, 0).getDate();
|
|
63
|
-
return Array.from({ length: count }, function (_, i) { return i + 1; });
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
get complete() {
|
|
67
|
-
return !!(this.year && this.month && this.day);
|
|
68
|
-
},
|
|
69
|
-
|
|
70
|
-
get isoValue() {
|
|
71
|
-
if (!this.complete) return "";
|
|
72
|
-
return this.year + "-" +
|
|
73
|
-
String(this.month).padStart(2, "0") + "-" +
|
|
74
|
-
String(this.day).padStart(2, "0");
|
|
75
|
-
},
|
|
76
|
-
|
|
77
|
-
// Named to match what the shared field binds to. Clears a day that the new
|
|
78
|
-
// month or year does not have — pick 31 January then switch to February and
|
|
79
|
-
// the 31st stops existing — then publishes.
|
|
80
|
-
onMonthChange() {
|
|
81
|
-
if (this.day && parseInt(this.day, 10) > this.dayOptions.length) this.day = "";
|
|
82
|
-
this.publish();
|
|
83
|
-
},
|
|
84
|
-
|
|
85
|
-
// WHAT THE DIRTY CHECK IS SHOWN, and why it is not isoValue.
|
|
86
|
-
//
|
|
87
|
-
// isoValue is "" for EVERY incomplete date, so publishing it made a whole
|
|
88
|
-
// class of edit invisible: on an empty birthday, choosing a month wrote ""
|
|
89
|
-
// over "" and the save bar never appeared. The operator reported exactly
|
|
90
|
-
// that. Choosing a month IS a change — the form just had no way to see it.
|
|
91
|
-
//
|
|
92
|
-
// So the signal is the three parts, which differ whenever any one of them
|
|
93
|
-
// does. It stays equal to isoValue for a COMPLETE date (same string, same
|
|
94
|
-
// shape) so a finished edit compares correctly against the value the page
|
|
95
|
-
// loaded with, and Discard restores it to that same string.
|
|
96
|
-
//
|
|
97
|
-
// `fields` belongs to the enclosing studioProfileForm — Alpine's scope
|
|
98
|
-
// chain makes it reachable and it is the SAME object, so assigning through
|
|
99
|
-
// it is what raises the save bar.
|
|
100
|
-
// THE EMPTY CASE IS NOT A PARTIAL ONE, and conflating them makes the page
|
|
101
|
-
// arrive already dirty. studioProfileForm's initial.birthday is "" for a
|
|
102
|
-
// person with no birthday on file, so an untouched empty field must also
|
|
103
|
-
// read "" — joining three blanks gives "--", which compares as a change and
|
|
104
|
-
// would raise the save bar on load, before anyone touched anything.
|
|
105
|
-
get signature() {
|
|
106
|
-
if (this.complete) return this.isoValue;
|
|
107
|
-
if (!this.year && !this.month && !this.day) return "";
|
|
108
|
-
return [this.year, this.month, this.day].join("-");
|
|
109
|
-
},
|
|
110
|
-
|
|
111
|
-
publish() {
|
|
112
|
-
if (this.fields) this.fields.birthday = this.signature;
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
// DISCARD MUST REACH THE SELECTS. studioProfileForm#discard reassigns its
|
|
116
|
-
// `fields` object, which restores every input bound with x-model — and the
|
|
117
|
-
// three selects are not, because they are bound to THIS component's own
|
|
118
|
-
// month/day/year. So Discard used to leave them showing the abandoned edit
|
|
119
|
-
// while the bar disappeared, and the hidden input went on carrying it: the
|
|
120
|
-
// next save would write the change the user had just discarded.
|
|
121
|
-
//
|
|
122
|
-
// Watching `fields.birthday` is what joins them. Discard sets it back to
|
|
123
|
-
// the initial string, this fires, and the selects follow — and because the
|
|
124
|
-
// guard below returns early when the value already matches, the publish →
|
|
125
|
-
// watch → publish path cannot loop.
|
|
126
|
-
init() {
|
|
127
|
-
var self = this;
|
|
128
|
-
this.publish();
|
|
129
|
-
this.$watch("fields.birthday", function (value) {
|
|
130
|
-
if (value === self.signature) return;
|
|
131
|
-
self.adopt(value);
|
|
132
|
-
});
|
|
133
|
-
},
|
|
134
|
-
|
|
135
|
-
// Takes an ISO date (or a blank) and puts the selects on it. Anything that
|
|
136
|
-
// is not a full date empties all three, which is what an initial blank and
|
|
137
|
-
// a cleared birthday both mean.
|
|
138
|
-
adopt(value) {
|
|
139
|
-
var m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value || ""));
|
|
140
|
-
this.year = m ? String(+m[1]) : "";
|
|
141
|
-
this.month = m ? String(+m[2]) : "";
|
|
142
|
-
this.day = m ? String(+m[3]) : "";
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
};
|
|
146
|
-
</script>
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
<%# The edit form's Alpine component.
|
|
2
|
-
|
|
3
|
-
Its own partial so the page above stays readable and this can be reasoned
|
|
4
|
-
about as behaviour rather than markup.
|
|
5
|
-
%>
|
|
6
|
-
<script>
|
|
7
|
-
window.studioProfileForm = function (initial) {
|
|
8
|
-
return {
|
|
9
|
-
alpine: true,
|
|
10
|
-
fields: Object.assign({}, initial),
|
|
11
|
-
initial: Object.assign({}, initial),
|
|
12
|
-
|
|
13
|
-
// Trimmed on both sides: whitespace is not a change, and the controller
|
|
14
|
-
// trims on the way in too — so the bar agrees with what saving would do.
|
|
15
|
-
changed(key) {
|
|
16
|
-
return String(this.fields[key] || "").trim() !== String(this.initial[key] || "").trim();
|
|
17
|
-
},
|
|
18
|
-
|
|
19
|
-
get changeCount() {
|
|
20
|
-
var self = this;
|
|
21
|
-
return Object.keys(this.fields).filter(function (k) { return self.changed(k); }).length;
|
|
22
|
-
},
|
|
23
|
-
|
|
24
|
-
get dirty() { return this.changeCount > 0; },
|
|
25
|
-
|
|
26
|
-
discard() {
|
|
27
|
-
this.fields = Object.assign({}, this.initial);
|
|
28
|
-
},
|
|
29
|
-
|
|
30
|
-
init() {
|
|
31
|
-
var self = this;
|
|
32
|
-
|
|
33
|
-
// Guard the exit BOTH ways. beforeunload covers a hard navigation or a
|
|
34
|
-
// closed tab; turbo:before-visit covers a Turbo Drive visit, which never
|
|
35
|
-
// fires beforeunload — an app with Turbo on would otherwise drop edits
|
|
36
|
-
// silently on any in-app link.
|
|
37
|
-
this._beforeUnload = function (e) {
|
|
38
|
-
if (!self.dirty) return;
|
|
39
|
-
e.preventDefault();
|
|
40
|
-
e.returnValue = "";
|
|
41
|
-
};
|
|
42
|
-
window.addEventListener("beforeunload", this._beforeUnload);
|
|
43
|
-
|
|
44
|
-
this._beforeVisit = function (e) {
|
|
45
|
-
if (!self.dirty) return;
|
|
46
|
-
if (!window.confirm("You have unsaved changes. Leave without saving?")) {
|
|
47
|
-
e.preventDefault();
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
document.addEventListener("turbo:before-visit", this._beforeVisit);
|
|
51
|
-
|
|
52
|
-
// Submitting is not leaving. Drop both guards before the form goes, or
|
|
53
|
-
// saving prompts you about the very changes you just chose to save.
|
|
54
|
-
this.$refs.form.addEventListener("submit", function () { self.teardown(); });
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
teardown() {
|
|
58
|
-
window.removeEventListener("beforeunload", this._beforeUnload);
|
|
59
|
-
document.removeEventListener("turbo:before-visit", this._beforeVisit);
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
destroy() { this.teardown(); }
|
|
63
|
-
};
|
|
64
|
-
};
|
|
65
|
-
</script>
|