@panphora/clayjs 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/clay.standalone.js +22327 -0
- package/entries/clay-data.js +1 -1
- package/package.json +9 -2
- package/src/core/etag.js +127 -0
- package/src/core/host-attrs.js +58 -3
- package/src/core/host-meta.js +20 -3
- package/src/core/is-edit-mode.js +14 -5
- package/src/core/save-conflict-notice.js +191 -0
- package/src/core/save-core.js +329 -7
- package/src/core/save.js +98 -4
- package/src/core/snapshot.js +38 -17
- package/src/core/stale-host-notice.js +95 -0
- package/src/lib/hostile-css.js +49 -0
- package/src/lib/root-attrs.js +62 -7
- package/src/loader-logic.js +44 -0
- package/src/loader.js +11 -5
- package/src/plugins/indicator.js +13 -2
- package/src/plugins/sortable.js +5 -5
- package/src/standalone.js +98 -0
- package/src/sync/live-sync.js +208 -15
- package/src/ui/index.js +7 -0
- package/src/vendor/hypercms.vendor.js +13 -13
- package/src/lib/load-vendor-script.js +0 -57
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { servedStaleToken } from "./host-attrs.js";
|
|
2
|
+
import { isEditMode } from "./is-edit-mode.js";
|
|
3
|
+
import onDomReady from "../lib/dom-ready.js";
|
|
4
|
+
import { set, make } from "../lib/hostile-css.js";
|
|
5
|
+
|
|
6
|
+
// The page's only visible word on a host too old to save to.
|
|
7
|
+
//
|
|
8
|
+
// This library reads one save-token name. A host older than that rename sends the
|
|
9
|
+
// other one, so there is no token here, and it also sets the owner cookie, so without
|
|
10
|
+
// help the page would open fully editable and 404 every save against a route that no
|
|
11
|
+
// longer matches. is-edit-mode.js takes editing away for that reason. This says why.
|
|
12
|
+
//
|
|
13
|
+
// It loads in the ALWAYS wave, not the edit-only one, which is the whole point: the
|
|
14
|
+
// case it exists for is precisely the case where edit mode is off, so a module gated on
|
|
15
|
+
// edit mode could never run in it. host-attrs.js also logs a line, and that line is for
|
|
16
|
+
// a developer. Somebody editing a document in a desktop app has no console open, and
|
|
17
|
+
// "why can I not edit this any more" is the question they are actually holding.
|
|
18
|
+
//
|
|
19
|
+
// One line, no choice to make: nothing on this page can fix it, so offering a button
|
|
20
|
+
// would be a lie. Dismissable, because after you have read it, it is only in the way.
|
|
21
|
+
|
|
22
|
+
const BG = "var(--clay-notice-bg,#222)";
|
|
23
|
+
const INK = "var(--clay-notice-ink,#fff)";
|
|
24
|
+
const EDGE = "var(--clay-notice-edge,rgba(255,255,255,.28))";
|
|
25
|
+
const FONT = "14px/1.45 system-ui,-apple-system,'Segoe UI',sans-serif";
|
|
26
|
+
|
|
27
|
+
const MESSAGE =
|
|
28
|
+
"This page can't be edited: the app serving it is out of date. " +
|
|
29
|
+
"Update HTML Clay to 1.9.0 or newer.";
|
|
30
|
+
|
|
31
|
+
let root = null;
|
|
32
|
+
|
|
33
|
+
// A phone keyboard shrinks the visual viewport but leaves fixed elements pinned to the
|
|
34
|
+
// layout viewport, so a bottom-anchored bar parks itself behind the keyboard. Same fix
|
|
35
|
+
// as the conflict notice.
|
|
36
|
+
function place() {
|
|
37
|
+
if (!root) return;
|
|
38
|
+
const vv = window.visualViewport;
|
|
39
|
+
const lift = vv ? Math.max(0, window.innerHeight - vv.height - vv.offsetTop) : 0;
|
|
40
|
+
set(root, "bottom", `calc(${16 + lift}px + env(safe-area-inset-bottom,0px))`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function dismiss() {
|
|
44
|
+
if (!root) return;
|
|
45
|
+
set(root, "display", "none");
|
|
46
|
+
window.visualViewport?.removeEventListener("resize", place);
|
|
47
|
+
window.visualViewport?.removeEventListener("scroll", place);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function build() {
|
|
51
|
+
root = make("div", [
|
|
52
|
+
"position:fixed", "left:50%", "transform:translateX(-50%)",
|
|
53
|
+
"z-index:2147483001", "display:flex", "align-items:center", "gap:10px",
|
|
54
|
+
"max-width:calc(100vw - 24px)", "flex-wrap:wrap", "justify-content:center",
|
|
55
|
+
"padding:9px 12px", "border-radius:10px",
|
|
56
|
+
`background:${BG}`, `color:${INK}`, `border:1px solid ${EDGE}`,
|
|
57
|
+
"box-shadow:0 6px 24px rgba(0,0,0,.32),0 1px 2px rgba(0,0,0,.24)",
|
|
58
|
+
`font:${FONT}`, "text-align:left",
|
|
59
|
+
]);
|
|
60
|
+
// Three markers, and each is load-bearing on a page that CAN save: this element is
|
|
61
|
+
// injected, so it is in no document on disk and must never reach one, never wake the
|
|
62
|
+
// watcher, and never ride out in a snapshot to somebody else's browser.
|
|
63
|
+
root.setAttribute("clay", "no-save no-watch no-snapshot");
|
|
64
|
+
root.setAttribute("data-clay-stale-host", "");
|
|
65
|
+
root.setAttribute("role", "alert");
|
|
66
|
+
|
|
67
|
+
root.append(make("span", ["margin-right:2px"], MESSAGE));
|
|
68
|
+
|
|
69
|
+
const close = make("button", [
|
|
70
|
+
"all:initial", "box-sizing:border-box", "cursor:pointer", `font:${FONT}`,
|
|
71
|
+
"color:" + INK, "opacity:.72", "padding:2px 6px", "border-radius:6px", "flex:none",
|
|
72
|
+
], "Dismiss");
|
|
73
|
+
close.type = "button";
|
|
74
|
+
close.setAttribute("aria-label", "Dismiss this message");
|
|
75
|
+
close.addEventListener("click", dismiss);
|
|
76
|
+
root.append(close);
|
|
77
|
+
|
|
78
|
+
document.body.appendChild(root);
|
|
79
|
+
place();
|
|
80
|
+
window.visualViewport?.addEventListener("resize", place);
|
|
81
|
+
window.visualViewport?.addEventListener("scroll", place);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function init() {
|
|
85
|
+
if (!servedStaleToken()) return;
|
|
86
|
+
// ?editmode=true outranks the stale-host check by design: that is a person at the
|
|
87
|
+
// keyboard asking for editing on this load, and is-edit-mode.js gives it to them.
|
|
88
|
+
// This notice never consulted that, so it appeared on a page that IS editable and
|
|
89
|
+
// told its reader the opposite. The message is only true while editing is actually
|
|
90
|
+
// off, so it is shown only then.
|
|
91
|
+
if (isEditMode) return;
|
|
92
|
+
build();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
onDomReady(init);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Styling that survives somebody else's stylesheet.
|
|
2
|
+
//
|
|
3
|
+
// Every declaration goes on as !important, and that is the whole reason anything this
|
|
4
|
+
// library draws on a stranger's page looks the way it was written. A plain inline style
|
|
5
|
+
// loses to an author rule carrying !important, and `button { ... !important }` is a
|
|
6
|
+
// thing real pages do: the first browser run of the conflict notice had both of its
|
|
7
|
+
// controls repainted in the host page's colours and font. Only an inline !important
|
|
8
|
+
// outranks an author !important.
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Apply `prop:value` rules to an element, each one !important.
|
|
12
|
+
* @param {Element} el
|
|
13
|
+
* @param {string[]} rules
|
|
14
|
+
*/
|
|
15
|
+
export function style(el, rules) {
|
|
16
|
+
for (const rule of rules) {
|
|
17
|
+
const at = rule.indexOf(":");
|
|
18
|
+
el.style.setProperty(rule.slice(0, at).trim(), rule.slice(at + 1).trim(), "important");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Set one property, !important.
|
|
24
|
+
*
|
|
25
|
+
* Same reason as above, from the other direction: a later assignment has to be able to
|
|
26
|
+
* beat the !important already sitting on the element, and a plain `el.style.foo = x`
|
|
27
|
+
* silently cannot.
|
|
28
|
+
*
|
|
29
|
+
* @param {Element} el
|
|
30
|
+
* @param {string} prop
|
|
31
|
+
* @param {string} value
|
|
32
|
+
*/
|
|
33
|
+
export function set(el, prop, value) {
|
|
34
|
+
el.style.setProperty(prop, value, "important");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build an element with those rules already on it.
|
|
39
|
+
* @param {string} tag
|
|
40
|
+
* @param {string[]} rules
|
|
41
|
+
* @param {string} [text]
|
|
42
|
+
* @returns {HTMLElement}
|
|
43
|
+
*/
|
|
44
|
+
export function make(tag, rules, text) {
|
|
45
|
+
const el = document.createElement(tag);
|
|
46
|
+
style(el, rules);
|
|
47
|
+
if (text) el.textContent = text;
|
|
48
|
+
return el;
|
|
49
|
+
}
|
package/src/lib/root-attrs.js
CHANGED
|
@@ -8,15 +8,70 @@
|
|
|
8
8
|
* peer's copy of them must never be applied.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
//
|
|
12
|
-
// per-tab and makes the host strip it before writing, so it never reaches
|
|
13
|
-
// But §9 bounds only the save path, and §10 fans a snapshot out to other editors'
|
|
11
|
+
// The save token, in the order a reader tries the two spellings. Spec §9 bounds it to
|
|
12
|
+
// per-file and per-tab and makes the host strip it before writing, so it never reaches
|
|
13
|
+
// disk. But §9 bounds only the save path, and §10 fans a snapshot out to other editors'
|
|
14
14
|
// browsers, which is the hole this module closes.
|
|
15
15
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
16
|
+
// Save tokens ONLY. host-attrs.js returns the first name it finds here and puts it
|
|
17
|
+
// straight into the save URL, so anything added to this list becomes a credential
|
|
18
|
+
// in a path. Durable identities go in HOST_IDENTITY_ATTRS, never in this one.
|
|
19
|
+
//
|
|
20
|
+
// One name, deliberately. §9 names exactly one save-token attribute.
|
|
21
|
+
//
|
|
22
|
+
// This is a knowing break, not an oversight. htmlclay is the only host that mints a save
|
|
23
|
+
// token at all (hyperclay and Hyperclay Local authorize by cookie and inject nothing),
|
|
24
|
+
// it serves both spellings only from 1.9.0, and a document loads this library from a
|
|
25
|
+
// rolling URL. So a document served by an htmlclay at or below 1.8.0 keeps edit mode,
|
|
26
|
+
// because that host also sets the isAdminOfCurrentResource cookie, and every save 404s,
|
|
27
|
+
// because that host registers only `POST /_/save/{token}`.
|
|
28
|
+
//
|
|
29
|
+
// Taken on purpose, and taken now because the cost only grows: htmlclay 1.8.0 is days
|
|
30
|
+
// old, the product is pre-launch, and the alternative is carrying a second credential
|
|
31
|
+
// name in the save path permanently, since "wait until the old hosts are gone" is a
|
|
32
|
+
// condition nobody ever measures. host-attrs.js says so plainly in the console when it
|
|
33
|
+
// finds the old name, so the failure names its own cause and its own fix.
|
|
34
|
+
//
|
|
35
|
+
// ⚠️ RELEASE ORDER: htmlclay 1.9.0, which injects both names, must publish BEFORE this.
|
|
36
|
+
export const SAVE_TOKEN_ATTRS = ["savetoken"];
|
|
37
|
+
|
|
38
|
+
// The pre-rename spelling. Never read as a credential, which is the point of it being
|
|
39
|
+
// here rather than above, and never removed from HOST_TOKEN_ATTRS below, which is a
|
|
40
|
+
// different job: a name a host may inject has to go on being stripped before a save and
|
|
41
|
+
// kept out of a peer's morph, or a live token gets written into a document or handed to
|
|
42
|
+
// another tab. htmlclay injects it forever, because documents frozen against an older
|
|
43
|
+
// client read it, so this name never leaves this file.
|
|
44
|
+
export const LEGACY_SAVE_TOKEN_ATTRS = ["htmlclaytoken"];
|
|
45
|
+
|
|
46
|
+
// Host-injected, but NOT credentials. This is htmlclay's durable file identity,
|
|
47
|
+
// stamped on every serve and absent from disk bytes, so it rides in the morph
|
|
48
|
+
// protection below for the same reason a token does: a morph of raw disk content
|
|
49
|
+
// would otherwise strip this tab's copy, and a peer's copy must never be applied.
|
|
50
|
+
//
|
|
51
|
+
// Two spellings, permanently, mirroring the token list above. htmlclay serves
|
|
52
|
+
// `documentid` and reads either, but a document saved before that rename holds
|
|
53
|
+
// `htmlclayid` on disk forever, and this list is what a morph consults. Knowing
|
|
54
|
+
// only the current name would leave every pre-rename file's identity unprotected,
|
|
55
|
+
// which is the failure this list exists to prevent.
|
|
56
|
+
//
|
|
57
|
+
// It was previously in the token list, where saveToken() returned it whenever a
|
|
58
|
+
// host minted no token of its own. That is reachable: htmlclay stamps the id on
|
|
59
|
+
// every serve but injects a token only for top-level document loads, and its save
|
|
60
|
+
// route strips only the token, so the id reaches disk. Any such file hosted
|
|
61
|
+
// somewhere tokenless made this library POST to /_/save/{id} with no cookie and
|
|
62
|
+
// hand edit mode to every visitor. Splitting this list off from the token list is
|
|
63
|
+
// what fixed that, and the split holds whatever token spellings are read above.
|
|
64
|
+
export const HOST_IDENTITY_ATTRS = ["documentid", "htmlclayid"];
|
|
65
|
+
|
|
66
|
+
// What a host may have injected, and therefore what has to be stripped before a save
|
|
67
|
+
// and kept out of an incoming morph. Wider than what is READ, on purpose: the old token
|
|
68
|
+
// spelling is still injected by every htmlclay, so it still has to be stripped, whether
|
|
69
|
+
// or not this library will accept it as a credential.
|
|
70
|
+
export const HOST_TOKEN_ATTRS = [
|
|
71
|
+
...SAVE_TOKEN_ATTRS,
|
|
72
|
+
...LEGACY_SAVE_TOKEN_ATTRS,
|
|
73
|
+
...HOST_IDENTITY_ATTRS,
|
|
74
|
+
];
|
|
20
75
|
|
|
21
76
|
// This library's own root state, and this tab's UI truth.
|
|
22
77
|
export const ROOT_LIBRARY_ATTRS = ["savestatus", "editmode", "pageowner"];
|
package/src/loader-logic.js
CHANGED
|
@@ -4,9 +4,14 @@ export const CORE_WAVES = {
|
|
|
4
4
|
"core/edit-mode.js", // both modes — matches today: 'edit-mode' is NOT in EDIT_MODE_ONLY
|
|
5
5
|
// (hyperclay.js:251-273 lists 'edit-mode-helpers', not 'edit-mode');
|
|
6
6
|
// toggleEditMode must exist in view mode, it's the way IN
|
|
7
|
+
// always, deliberately: it speaks only when the host is too old to save to, which
|
|
8
|
+
// is exactly the case where edit mode is off, so the editOnly wave would never
|
|
9
|
+
// reach it. It draws nothing on any other page.
|
|
10
|
+
"core/stale-host-notice.js",
|
|
7
11
|
],
|
|
8
12
|
editOnly: [
|
|
9
13
|
"core/snapshot.js", "core/save-core.js", "core/save.js",
|
|
14
|
+
"core/save-conflict-notice.js",
|
|
10
15
|
"core/unsaved-warning.js", "core/persist.js",
|
|
11
16
|
"core/admin-attrs.js", "core/autosave.js",
|
|
12
17
|
"attrs/save-freeze.js", "attrs/onaftersave.js", "attrs/refetch-on-save.js",
|
|
@@ -30,6 +35,45 @@ export const PLUGIN_PATHS = {
|
|
|
30
35
|
demo: { path: "plugins/demo.js", editOnly: false, default: false },
|
|
31
36
|
};
|
|
32
37
|
|
|
38
|
+
// One literal import per module the loader can ask for, so a bundler can see the
|
|
39
|
+
// whole graph. The loader used to build each specifier at runtime
|
|
40
|
+
// (`import(base + "/src/" + path)`), which no bundler can follow, and that alone
|
|
41
|
+
// stood between clayjs and the single-file build. A relative specifier resolves
|
|
42
|
+
// against this file, so on clayjs.com the URLs fetched are exactly the ones the
|
|
43
|
+
// computed form produced. The thunks keep every import lazy: nothing evaluates
|
|
44
|
+
// until the loader asks, in the loader's order, which is load-bearing.
|
|
45
|
+
// tests/unit/loader-modules.test.js proves every path in CORE_WAVES and
|
|
46
|
+
// PLUGIN_PATHS has an entry here and that each entry imports the file it names.
|
|
47
|
+
export const MODULES = {
|
|
48
|
+
"core/is-edit-mode.js": () => import("./core/is-edit-mode.js"),
|
|
49
|
+
"lib/region-policy.js": () => import("./lib/region-policy.js"),
|
|
50
|
+
"lib/mutation.js": () => import("./lib/mutation.js"),
|
|
51
|
+
"core/edit-mode.js": () => import("./core/edit-mode.js"),
|
|
52
|
+
"core/stale-host-notice.js": () => import("./core/stale-host-notice.js"),
|
|
53
|
+
"core/snapshot.js": () => import("./core/snapshot.js"),
|
|
54
|
+
"core/save-core.js": () => import("./core/save-core.js"),
|
|
55
|
+
"core/save.js": () => import("./core/save.js"),
|
|
56
|
+
"core/save-conflict-notice.js": () => import("./core/save-conflict-notice.js"),
|
|
57
|
+
"core/unsaved-warning.js": () => import("./core/unsaved-warning.js"),
|
|
58
|
+
"core/persist.js": () => import("./core/persist.js"),
|
|
59
|
+
"core/admin-attrs.js": () => import("./core/admin-attrs.js"),
|
|
60
|
+
"core/autosave.js": () => import("./core/autosave.js"),
|
|
61
|
+
"attrs/save-freeze.js": () => import("./attrs/save-freeze.js"),
|
|
62
|
+
"attrs/onaftersave.js": () => import("./attrs/onaftersave.js"),
|
|
63
|
+
"attrs/refetch-on-save.js": () => import("./attrs/refetch-on-save.js"),
|
|
64
|
+
"lib/cache-bust.js": () => import("./lib/cache-bust.js"),
|
|
65
|
+
"vendor/richclay.vendor.js": () => import("./vendor/richclay.vendor.js"),
|
|
66
|
+
"plugins/indicator.js": () => import("./plugins/indicator.js"),
|
|
67
|
+
"sync/live-sync.js": () => import("./sync/live-sync.js"),
|
|
68
|
+
"plugins/sortable.js": () => import("./plugins/sortable.js"),
|
|
69
|
+
"plugins/undo.js": () => import("./plugins/undo.js"),
|
|
70
|
+
"vendor/hypercms.vendor.js": () => import("./vendor/hypercms.vendor.js"),
|
|
71
|
+
"vendor/quickcrop.vendor.js": () => import("./vendor/quickcrop.vendor.js"),
|
|
72
|
+
"plugins/upload.js": () => import("./plugins/upload.js"),
|
|
73
|
+
"plugins/wire.js": () => import("./plugins/wire.js"),
|
|
74
|
+
"plugins/demo.js": () => import("./plugins/demo.js"),
|
|
75
|
+
};
|
|
76
|
+
|
|
33
77
|
const PLUGIN_ORDER = ["richclay", "indicator", "sortable", "undo", "quickcrop", "upload", "cms", "sync", "wire", "demo"];
|
|
34
78
|
|
|
35
79
|
// A plugin that cannot do its whole job alone. hypercms reads the cropper through
|
package/src/loader.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import { resolveModules } from "./loader-logic.js";
|
|
1
|
+
import { resolveModules, MODULES } from "./loader-logic.js";
|
|
2
2
|
import onDomReady from "./lib/dom-ready.js";
|
|
3
3
|
|
|
4
4
|
function domReady() {
|
|
5
5
|
return new Promise((resolve) => onDomReady(resolve));
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
// `base` has been unused since 1.1.0 (the loader imports through MODULES) and
|
|
9
|
+
// stays anyway: /v1/clay.js and /v1/src/loader.js are cached independently, so
|
|
10
|
+
// for a while after a deploy a browser can pair either file with the other's
|
|
11
|
+
// previous release. A call shape that differs between them leaves the page with
|
|
12
|
+
// no clayjs at all.
|
|
8
13
|
export async function boot(base, params, readyResolve) {
|
|
9
14
|
await domReady(); // Mutation observes document.body
|
|
10
15
|
// unconditionally, and a <head> placement
|
|
11
16
|
// would otherwise observe null
|
|
12
17
|
|
|
13
|
-
const editMode = await
|
|
18
|
+
const editMode = await MODULES["core/is-edit-mode.js"]();
|
|
14
19
|
const { isEditMode, isOwner } = editMode;
|
|
15
20
|
|
|
16
21
|
// richclay's vendor build detects edit mode via this legacy global; set it
|
|
@@ -19,13 +24,13 @@ export async function boot(base, params, readyResolve) {
|
|
|
19
24
|
// and a conflicting leftover would make richclay disable itself.
|
|
20
25
|
window.__hyperclayEditMode = isEditMode;
|
|
21
26
|
|
|
22
|
-
const regionPolicy = await
|
|
27
|
+
const regionPolicy = await MODULES["lib/region-policy.js"]();
|
|
23
28
|
|
|
24
29
|
const plan = resolveModules(params, isEditMode);
|
|
25
30
|
const loaded = {};
|
|
26
31
|
|
|
27
32
|
for (const path of plan.core) {
|
|
28
|
-
loaded[path] = await
|
|
33
|
+
loaded[path] = await MODULES[path](); // sequential: order is load-bearing
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
assembleCore(loaded, { isEditMode, isOwner }, regionPolicy); // window.clay MUST be assembled
|
|
@@ -39,7 +44,7 @@ export async function boot(base, params, readyResolve) {
|
|
|
39
44
|
// `ready` exports, never their evaluation.)
|
|
40
45
|
let mod;
|
|
41
46
|
try {
|
|
42
|
-
mod = await
|
|
47
|
+
mod = await MODULES[path]();
|
|
43
48
|
} catch (err) {
|
|
44
49
|
console.error(`clayjs: plugin "${path}" failed to load, continuing without it:`, err);
|
|
45
50
|
continue;
|
|
@@ -81,6 +86,7 @@ function assembleCore(loaded, { isEditMode, isOwner }, regionPolicy) {
|
|
|
81
86
|
if (save) {
|
|
82
87
|
const saveFn = save.savePage || save.default;
|
|
83
88
|
saveFn.force = save.savePageForce;
|
|
89
|
+
saveFn.overwrite = save.saveOverwritingConflict;
|
|
84
90
|
clay.save = saveFn;
|
|
85
91
|
}
|
|
86
92
|
if (snapshot) {
|
package/src/plugins/indicator.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { isEditMode } from "../core/is-edit-mode.js";
|
|
2
2
|
import onDomReady from "../lib/dom-ready.js";
|
|
3
3
|
|
|
4
|
+
// No 'conflict' here on purpose. core/save-conflict-notice.js owns that state now,
|
|
5
|
+
// and it ships in every document rather than only the ones that turned this chip on.
|
|
6
|
+
// Both listen to clay:save-conflict, so keeping a label here put a chip in the corner
|
|
7
|
+
// saying the same thing as the bar at the same moment. Dropping it from this side
|
|
8
|
+
// leaves core unaware that this plugin exists, which is the direction that
|
|
9
|
+
// dependency has to point.
|
|
4
10
|
const LABELS = {
|
|
5
11
|
saving: "Saving…",
|
|
6
12
|
saved: "Saved",
|
|
@@ -8,6 +14,11 @@ const LABELS = {
|
|
|
8
14
|
offline: "Offline, not saved",
|
|
9
15
|
};
|
|
10
16
|
|
|
17
|
+
// States that stay on screen instead of fading, because 'saving' is still in flight.
|
|
18
|
+
const STICKY = new Set(["saving"]);
|
|
19
|
+
|
|
20
|
+
const ALARMING = new Set(["error", "offline"]);
|
|
21
|
+
|
|
11
22
|
let el = null;
|
|
12
23
|
let hideTimer = null;
|
|
13
24
|
|
|
@@ -34,11 +45,11 @@ function show(state) {
|
|
|
34
45
|
const node = ensure();
|
|
35
46
|
node.textContent = LABELS[state];
|
|
36
47
|
node.dataset.state = state;
|
|
37
|
-
node.style.background = state
|
|
48
|
+
node.style.background = ALARMING.has(state)
|
|
38
49
|
? "var(--clay-indicator-error-bg,#7a3b28)" : "var(--clay-indicator-bg,#2e2b27)";
|
|
39
50
|
node.style.opacity = "1";
|
|
40
51
|
clearTimeout(hideTimer);
|
|
41
|
-
if (state
|
|
52
|
+
if (!STICKY.has(state)) hideTimer = setTimeout(() => { node.style.opacity = "0"; }, 2200);
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
function init() {
|
package/src/plugins/sortable.js
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { isEditMode } from "../core/is-edit-mode.js";
|
|
18
18
|
import Mutation from "../lib/mutation.js";
|
|
19
|
-
import { getVendorUrl } from "../lib/load-vendor-script.js";
|
|
20
19
|
|
|
21
20
|
function makeSortable(sortableElem, Sortable) {
|
|
22
21
|
let options = {};
|
|
@@ -89,10 +88,11 @@ function makeSortable(sortableElem, Sortable) {
|
|
|
89
88
|
async function init() {
|
|
90
89
|
if (!isEditMode) return;
|
|
91
90
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
91
|
+
// Sortable's UMD header picks its target at run time: window.Sortable when no
|
|
92
|
+
// module system is present (a module load, and the single-file build), a
|
|
93
|
+
// default export when a bundler hands it one. Cover both.
|
|
94
|
+
const mod = await import('../vendor/Sortable.vendor.js');
|
|
95
|
+
const Sortable = window.Sortable || mod.default;
|
|
96
96
|
|
|
97
97
|
// Set up sortable on page load
|
|
98
98
|
document.querySelectorAll('[sortable]').forEach(el => makeSortable(el, Sortable));
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/* clayjs standalone: the entry of the single-file build. https://clayjs.com/offline
|
|
2
|
+
|
|
3
|
+
Twin of entries/clay.js. That bootstrap derives a base URL from its own script
|
|
4
|
+
tag and imports the loader at runtime; this one is bundled together with the
|
|
5
|
+
loader and every module it can ask for (see MODULES in loader-logic.js), so
|
|
6
|
+
`plugins=` decides what runs, never what downloads. The bootstrap logic below
|
|
7
|
+
(window.clay, `ready`, `__booted`, the retry path) is the same code as clay.js
|
|
8
|
+
and must stay in step with it; tests/unit/bootstrap-twins.test.js compares the
|
|
9
|
+
shared pieces. */
|
|
10
|
+
import { boot } from "./loader.js";
|
|
11
|
+
import onDomReady from "./lib/dom-ready.js";
|
|
12
|
+
|
|
13
|
+
(function () {
|
|
14
|
+
// Suppress every vendored bundle's window auto-export; the loader assembles
|
|
15
|
+
// window.clay explicitly.
|
|
16
|
+
window.__hyperclayNoAutoExport = true;
|
|
17
|
+
|
|
18
|
+
// Merge into any window.clay a satellite already created; never replace it.
|
|
19
|
+
var clay = window.clay = window.clay || {};
|
|
20
|
+
// A second tag is a no-op: the original boot's `ready` promise survives.
|
|
21
|
+
// A FAILED boot resets the sentinel so a corrected tag can retry; the retry
|
|
22
|
+
// resolves the ORIGINAL `ready` promise via the stashed resolver.
|
|
23
|
+
if (clay.__booted) return;
|
|
24
|
+
|
|
25
|
+
function mintReady() {
|
|
26
|
+
clay.ready = new Promise(function (res, rej) {
|
|
27
|
+
clay.__readyResolve = res;
|
|
28
|
+
clay.__readyReject = rej;
|
|
29
|
+
});
|
|
30
|
+
// Nobody may be awaiting a failed boot's promise, and an unhandled rejection
|
|
31
|
+
// in the console is noise on top of the error we already logged.
|
|
32
|
+
clay.ready.catch(function () {});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!clay.ready) mintReady();
|
|
36
|
+
|
|
37
|
+
// clay.js needs its own URL to find the loader; this file needs it only for the
|
|
38
|
+
// query string, so pasted inline into the page it boots with the defaults.
|
|
39
|
+
var script = document.currentScript;
|
|
40
|
+
var params = script && script.src ? new URL(script.src, location.href).searchParams : new URLSearchParams();
|
|
41
|
+
clay.__booted = true;
|
|
42
|
+
|
|
43
|
+
function domReady() {
|
|
44
|
+
return new Promise(function (r) { onDomReady(r); });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The module satellites, on the terms of their own entry scripts: each is a
|
|
48
|
+
// promise on clay.loaded, the ones that touch document.body wait for DOM
|
|
49
|
+
// ready, and events waits for dom because [onrender] handlers reach for its
|
|
50
|
+
// element helpers the moment events lands.
|
|
51
|
+
function satellite(name, promise) {
|
|
52
|
+
var p = promise.catch(function (err) {
|
|
53
|
+
console.error("clayjs: " + name + " failed to load:", err);
|
|
54
|
+
throw err;
|
|
55
|
+
});
|
|
56
|
+
// Mark handled: a failed satellite must not emit an unhandled rejection.
|
|
57
|
+
// Consumers who await clay.loaded[name] still get the error.
|
|
58
|
+
p.catch(function () {});
|
|
59
|
+
clay.loaded[name] = p;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
clay.loaded = clay.loaded || {};
|
|
63
|
+
// The two generated satellites are classic scripts that assemble themselves
|
|
64
|
+
// onto window.clay when they run, and they carry no guard of their own, so
|
|
65
|
+
// they run here, behind the sentinel, and not as hoisted static imports: a
|
|
66
|
+
// duplicate tag would otherwise mount a second Sap runtime on the page. For
|
|
67
|
+
// the same reason a tag that already registered one (a leftover
|
|
68
|
+
// <script src="sap.js"> above this one) wins, and the copy in here stays idle.
|
|
69
|
+
if (!clay.loaded.data) satellite("data", import("../entries/clay-data.js"));
|
|
70
|
+
if (!clay.loaded.sap) satellite("sap", import("../entries/sap.js"));
|
|
71
|
+
satellite("dom", import("./dom/dom-helpers.js"));
|
|
72
|
+
satellite("utils", import("./utils/index.js"));
|
|
73
|
+
satellite("internals", import("./internals/index.js"));
|
|
74
|
+
satellite("all", import("./dom/all.js").then(function (m) {
|
|
75
|
+
window.All = m.default; // interop carve-out, as in entries/all.js
|
|
76
|
+
clay.All = m.default;
|
|
77
|
+
return m.default;
|
|
78
|
+
}));
|
|
79
|
+
satellite("ui", domReady().then(function () { return import("./ui/index.js"); }));
|
|
80
|
+
satellite("options", domReady().then(function () { return import("./options/options.js"); }));
|
|
81
|
+
satellite("events", domReady()
|
|
82
|
+
.then(function () { return clay.loaded.dom && clay.loaded.dom.catch(function () {}); })
|
|
83
|
+
.then(function () { return import("./events/index.js"); }));
|
|
84
|
+
|
|
85
|
+
// The first argument is the loader's unused `base`; see boot() in loader.js.
|
|
86
|
+
boot(null, params, clay.__readyResolve).catch(function (err) {
|
|
87
|
+
clay.__booted = false;
|
|
88
|
+
console.error("clayjs failed to load:", err);
|
|
89
|
+
// Settle the promise so `await clay.ready` fails loudly instead of hanging,
|
|
90
|
+
// then mint a fresh one for a corrected retry tag. See entries/clay.js. A
|
|
91
|
+
// retry re-evaluates this whole file, satellites included; the path is
|
|
92
|
+
// reachable only when a bundled module throws on evaluation, which is a
|
|
93
|
+
// clayjs bug, not a page condition.
|
|
94
|
+
var reject = clay.__readyReject;
|
|
95
|
+
mintReady();
|
|
96
|
+
if (reject) reject(err);
|
|
97
|
+
});
|
|
98
|
+
})();
|