copy_tuner_client 2.3.0 → 2.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b141908e7fa8f6702cf9fccc712b88440af9aa96cc25c8b7875b3269d199825
4
- data.tar.gz: c7b3f527ba59c387f721364fe0e2689fd849b99b33f4e53557611f23af828794
3
+ metadata.gz: 803784553a4dbeb3916ccdffef8ebe40626644c18b1ac7fa0e42ff9158d9ae1a
4
+ data.tar.gz: aa985dc5ac60c2f137fe95a9fd5c3c15fd7e8667e7b4f183d49f1e89ba448aa7
5
5
  SHA512:
6
- metadata.gz: cae2e31ecad418c08b1143f57bd5acf83b574ae5ca4aac1526e594ce911e7d37c98b7098a5a4d1bcaa4cacc133368764712525d80ef63b7dac4390bed5858093
7
- data.tar.gz: 06e3b11bb99dc6162a0a1110999e1fe2ab4bc3dde42cecfa8ecbe0656a0861b7ea17f659c0ef1791f4df1601cd10e7b465eac755d2ae5305d22e78e0ffe260c4
6
+ metadata.gz: 321d3cc14cfd86127ca2bbe204fa89cd76f152a9770991ed0d8939f6b7724500b1bd7c92dca63b615e4ae741b2652ef0176f60e4c6342afd0a1c718ae8503ddc
7
+ data.tar.gz: 3860c2738c49a2c7fd4af233ba3f062fda3fff1a6d789a851a1f2973029e4cbc201f6d61898ba05f63bf9f79fd5e19d2b8a40ddf387f58d0699e0475d2d08f8c
@@ -1,69 +1,50 @@
1
1
  //#region src/styles.ts
2
- var e = navigator.platform.toUpperCase().includes("MAC"), t = (e) => !!(e.offsetWidth || e.offsetHeight || e.getClientRects().length > 0), n = (e) => {
3
- let t = e.getBoundingClientRect();
4
- return {
5
- top: t.top + (window.pageYOffset - document.documentElement.clientTop),
6
- left: t.left + (window.pageXOffset - document.documentElement.clientLeft)
7
- };
8
- }, r = (e) => {
9
- if (!t(e)) return null;
10
- let r = n(e);
11
- return r.right = r.left + e.offsetWidth, r.bottom = r.top + e.offsetHeight, {
12
- left: r.left,
13
- top: r.top,
14
- width: r.right - r.left,
15
- height: r.bottom - r.top
16
- };
17
- }, i = (e, t) => {
18
- let n;
19
- return (...r) => {
20
- clearTimeout(n), n = setTimeout(() => e(...r), t);
21
- };
22
- }, a = () => Array.from(document.querySelectorAll("[data-copyray-key]")).map((e) => ({
2
+ var e = () => Array.from(document.querySelectorAll("[data-copyray-key]")).map((e) => ({
23
3
  keys: (e.getAttribute("data-copyray-key") ?? "").split(",").filter(Boolean),
24
4
  element: e
25
- })), o = class extends HTMLElement {
5
+ })), t = class extends HTMLElement {
26
6
  #e = () => {};
27
7
  #t = () => {};
28
8
  #n;
29
9
  #r;
30
- #i;
10
+ #i = /* @__PURE__ */ new Map();
11
+ #a = null;
31
12
  constructor() {
32
13
  super();
33
14
  let e = this.attachShadow({ mode: "open" }), t = document.createElement("style");
34
- t.textContent = "\n:host {\n position: absolute;\n top: 0;\n left: 0;\n width: 0;\n height: 0;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n.backdrop {\n position: fixed;\n inset: 0;\n background-image: radial-gradient(\n ellipse farthest-corner at center,\n rgba(0, 0, 0, 0.4) 10%,\n rgba(0, 0, 0, 0.8) 100%\n );\n z-index: 9000;\n}\n\n.specimen {\n position: absolute;\n background: rgba(255, 50, 50, 0.1);\n outline: 1px solid rgba(255, 50, 50, 0.8);\n outline-offset: -1px;\n color: #666;\n font-family: 'Helvetica Neue', sans-serif;\n font-size: 13px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);\n z-index: 2000000000;\n}\n\n.specimen:hover {\n cursor: pointer;\n background: rgba(255, 50, 50, 0.4);\n}\n\n.specimen-handle {\n float: left;\n margin: 0 2px 2px 0;\n background: rgba(255, 50, 50, 0.8);\n padding: 0 3px;\n color: #fff;\n font-size: 10px;\n cursor: pointer;\n}\n\n.toggle-button {\n display: block;\n position: fixed;\n left: 0;\n bottom: 0;\n color: white;\n background: black;\n padding: 12px 16px;\n border-radius: 0 10px 0 0;\n opacity: 0;\n transition: opacity 0.6s ease-in-out;\n z-index: 10000;\n font-size: 12px;\n cursor: pointer;\n text-decoration: none;\n}\n\n.toggle-button:hover {\n opacity: 1;\n}\n\n@media screen and (max-width: 480px) {\n .toggle-button {\n display: none;\n }\n}\n", e.append(t), this.#n = document.createElement("div"), this.#n.classList.add("backdrop"), this.#n.addEventListener("click", () => this.hide()), this.#r = document.createElement("div"), this.#r.classList.add("specimens"), this.#i = document.createElement("a"), this.#i.classList.add("toggle-button"), this.#i.textContent = "Open CopyTuner", this.#i.addEventListener("click", () => this.#t()), e.append(this.#n, this.#r, this.#i), this.hide();
15
+ t.textContent = "\n:host {\n position: absolute;\n width: 0;\n height: 0;\n}\n\n.backdrop {\n position: fixed;\n inset: 0;\n background-image: radial-gradient(\n ellipse farthest-corner at center,\n rgba(0, 0, 0, 0.4) 10%,\n rgba(0, 0, 0, 0.8) 100%\n );\n z-index: 9000;\n}\n\n.specimen {\n position: fixed;\n background: rgba(255, 50, 50, 0.1);\n outline: 1px solid rgba(255, 50, 50, 0.8);\n outline-offset: -1px;\n color: #666;\n font-family: 'Helvetica Neue', sans-serif;\n font-size: 13px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);\n z-index: 2000000000;\n}\n\n.specimen:hover {\n cursor: pointer;\n background: rgba(255, 50, 50, 0.4);\n}\n\n.specimen[hidden] {\n display: none;\n}\n\n.specimen-handle {\n float: left;\n margin: 0 2px 2px 0;\n background: rgba(255, 50, 50, 0.8);\n padding: 0 3px;\n color: #fff;\n font-size: 10px;\n cursor: pointer;\n}\n", e.append(t), this.#n = document.createElement("div"), this.#n.classList.add("backdrop"), this.#n.addEventListener("click", () => this.#t()), this.#r = document.createElement("div"), e.append(this.#n, this.#r);
35
16
  }
36
17
  set onOpen(e) {
37
18
  this.#e = e;
38
19
  }
39
- set onToggle(e) {
20
+ set onClose(e) {
40
21
  this.#t = e;
41
22
  }
42
- get isShowing() {
43
- return !this.#n.hidden;
44
- }
45
23
  show() {
46
- this.reset(), this.#n.hidden = !1;
47
- for (let { element: e, keys: t } of a()) {
48
- let n = this.makeBox(e, t);
49
- n && this.#r.append(n);
50
- }
24
+ this.hide();
25
+ for (let { element: t, keys: n } of e()) this.#i.set(t, this.makeBox(n));
26
+ this.#s(), this.#r.append(...this.#i.values()), document.addEventListener("scroll", this.#o, {
27
+ capture: !0,
28
+ passive: !0
29
+ }), window.addEventListener("resize", this.#o, { passive: !0 });
51
30
  }
52
31
  hide() {
53
- this.reset(), this.#n.hidden = !0;
32
+ document.removeEventListener("scroll", this.#o, { capture: !0 }), window.removeEventListener("resize", this.#o), this.#a !== null && (cancelAnimationFrame(this.#a), this.#a = null), this.#i.clear(), this.#r.replaceChildren();
54
33
  }
55
- reset() {
56
- this.#r.replaceChildren();
34
+ #o = () => {
35
+ this.#a === null && (this.#a = requestAnimationFrame(() => {
36
+ this.#a = null, this.#s();
37
+ }));
38
+ };
39
+ #s() {
40
+ let e = Array.from(this.#i, ([e, t]) => [t, e.getBoundingClientRect()]);
41
+ for (let [t, n] of e) t.hidden = n.width === 0 && n.height === 0, !t.hidden && (t.style.left = `${n.left}px`, t.style.top = `${n.top}px`, t.style.width = `${n.width}px`, t.style.height = `${n.height}px`);
57
42
  }
58
- makeBox(e, t) {
59
- let n = r(e);
60
- if (n === null) return null;
61
- let i = document.createElement("div");
62
- i.classList.add("specimen"), i.style.left = `${n.left}px`, i.style.top = `${n.top}px`, i.style.width = `${n.width}px`, i.style.height = `${n.height}px`;
63
- let { position: a, top: o, left: s } = getComputedStyle(e);
64
- a === "fixed" && (i.style.position = "fixed", i.style.top = o, i.style.left = s), i.addEventListener("click", () => this.#e(t[0]));
65
- for (let e of t) i.append(this.makeLabel(e));
66
- return i;
43
+ makeBox(e) {
44
+ let t = document.createElement("div");
45
+ t.classList.add("specimen"), t.addEventListener("click", () => this.#e(e[0]));
46
+ for (let n of e) t.append(this.makeLabel(n));
47
+ return t;
67
48
  }
68
49
  makeLabel(e) {
69
50
  let t = document.createElement("div");
@@ -71,30 +52,29 @@ var e = navigator.platform.toUpperCase().includes("MAC"), t = (e) => !!(e.offset
71
52
  t.stopPropagation(), this.#e(e);
72
53
  }), t;
73
54
  }
74
- }, s = class extends HTMLElement {
55
+ }, n = navigator.platform.toUpperCase().includes("MAC"), r = (e, t) => {
56
+ let n;
57
+ return (...r) => {
58
+ clearTimeout(n), n = setTimeout(() => e(...r), t);
59
+ };
60
+ }, i = class extends HTMLElement {
75
61
  #e = () => {};
76
62
  #t;
77
63
  #n;
78
64
  constructor() {
79
65
  super(), this.attachShadow({ mode: "open" });
80
66
  }
81
- connectedCallback() {
82
- this.hidden = !0;
83
- }
84
- init({ url: e, data: t, keysSkipped: n, onOpen: r }) {
85
- this.#e = r;
67
+ init({ url: e, data: t, keysSkipped: n, onOpen: i }) {
68
+ this.#e = i;
86
69
  let a = this.shadowRoot, o = document.createElement("style");
87
- o.textContent = "\n:host {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n height: 40px;\n padding: 0 8px;\n background: #222;\n font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;\n font-weight: 200;\n color: #fff;\n z-index: 2147483647;\n box-shadow: 0 -1px 0 rgba(255, 255, 255, 0.1), inset 0 2px 6px rgba(0, 0, 0, 0.8);\n background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.3));\n box-sizing: border-box;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n.log-menu {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 40px;\n max-height: calc(100vh - 40px);\n background: #222;\n color: #fff;\n overflow-y: auto;\n}\n\n.log-menu[hidden] {\n display: none;\n}\n\n.log-menu tbody td {\n padding: 2px 8px;\n}\n\n.log-menu tbody tr {\n cursor: pointer;\n}\n\n.log-menu tbody tr:hover {\n background: #444;\n}\n\n.log-menu tbody tr[hidden] {\n display: none;\n}\n\n.button {\n position: relative;\n display: inline-block;\n color: #fff;\n margin: 8px 1px;\n height: 24px;\n line-height: 24px;\n padding: 0 8px;\n font-size: 14px;\n cursor: pointer;\n vertical-align: middle;\n background-color: #444;\n background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.2));\n border-radius: 2px;\n box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.2),\n inset 0 0 2px rgba(255, 255, 255, 0.2);\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.4);\n text-decoration: none;\n}\n\n.button:hover,\n.button:focus {\n color: #fff;\n text-decoration: none;\n background-color: #555;\n}\n\n.notice {\n display: inline-block;\n margin: 8px;\n font-size: 13px;\n line-height: 24px;\n vertical-align: middle;\n color: #ffd24d;\n}\n\n.search {\n appearance: none;\n border: none;\n border-radius: 2px;\n background-image: linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0));\n box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.2), inset 0 0 2px rgba(0, 0, 0, 0.2);\n padding: 2px 8px;\n margin: 0;\n line-height: 20px;\n vertical-align: middle;\n color: black;\n width: auto;\n height: auto;\n font-size: 14px;\n}\n", a.append(o);
70
+ o.textContent = "\n:host {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n height: 40px;\n padding: 0 8px;\n background: #222;\n font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;\n font-weight: 200;\n color: #fff;\n z-index: 2147483647;\n box-shadow: 0 -1px 0 rgba(255, 255, 255, 0.1), inset 0 2px 6px rgba(0, 0, 0, 0.8);\n background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.3));\n box-sizing: border-box;\n}\n\n.log-menu {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 40px;\n max-height: calc(100vh - 40px);\n background: #222;\n color: #fff;\n overflow-y: auto;\n}\n\n.log-menu[hidden] {\n display: none;\n}\n\n.log-menu tbody td {\n padding: 2px 8px;\n}\n\n.log-menu tbody tr {\n cursor: pointer;\n}\n\n.log-menu tbody tr:hover {\n background: #444;\n}\n\n.log-menu tbody tr[hidden] {\n display: none;\n}\n\n.button {\n position: relative;\n display: inline-block;\n color: #fff;\n margin: 8px 1px;\n height: 24px;\n line-height: 24px;\n padding: 0 8px;\n font-size: 14px;\n cursor: pointer;\n vertical-align: middle;\n background-color: #444;\n background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.2));\n border-radius: 2px;\n box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.2),\n inset 0 0 2px rgba(255, 255, 255, 0.2);\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.4);\n text-decoration: none;\n}\n\n.button:hover,\n.button:focus {\n color: #fff;\n text-decoration: none;\n background-color: #555;\n}\n\n.notice {\n display: inline-block;\n margin: 8px;\n font-size: 13px;\n line-height: 24px;\n vertical-align: middle;\n color: #ffd24d;\n}\n\n.search {\n appearance: none;\n border: none;\n border-radius: 2px;\n background-image: linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0));\n box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.2), inset 0 0 2px rgba(0, 0, 0, 0.2);\n padding: 2px 8px;\n margin: 0;\n line-height: 20px;\n vertical-align: middle;\n color: black;\n width: auto;\n height: auto;\n font-size: 14px;\n}\n", a.append(o);
88
71
  let s = this.makeButton("CopyTuner", e, "_blank"), c = this.makeButton("Sync", "/copytuner", "_blank"), l = this.makeButton("Translations in this page", "javascript:void(0)");
89
72
  this.#t = document.createElement("input"), this.#t.type = "text", this.#t.classList.add("search"), this.#t.placeholder = "search", a.append(s, c, l, this.#t), this.#n = this.makeLogMenu(t), a.append(this.#n), n && this.appendSkippedNotice(), l.addEventListener("click", (e) => {
90
73
  e.preventDefault(), this.toggleLogMenu();
91
- }), this.#t.addEventListener("input", i(this.onSearch.bind(this), 250));
74
+ }), this.#t.addEventListener("input", r(this.onSearch.bind(this), 250));
92
75
  }
93
76
  show() {
94
- this.hidden = !1, this.#t.focus();
95
- }
96
- hide() {
97
- this.hidden = !0;
77
+ this.#t.focus();
98
78
  }
99
79
  makeButton(e, t, n) {
100
80
  let r = document.createElement("a");
@@ -135,30 +115,41 @@ var e = navigator.platform.toUpperCase().includes("MAC"), t = (e) => !!(e.offset
135
115
  let t = [...this.#n.querySelectorAll("tr")];
136
116
  for (let n of t) n.hidden = !(e === "" || [...n.querySelectorAll("td")].some((t) => (t.textContent ?? "").includes(e)));
137
117
  }
118
+ }, a = class extends HTMLElement {
119
+ dialog;
120
+ #e = () => {};
121
+ constructor() {
122
+ super();
123
+ let e = this.attachShadow({ mode: "open" }), t = document.createElement("style");
124
+ t.textContent = "\n/* transform / filter / perspective / contain は置かない(理由は OVERLAY_STYLES のコメント参照) */\ndialog {\n position: fixed;\n inset: 0;\n width: 100%;\n height: 100%;\n max-width: none;\n max-height: none;\n margin: 0;\n border: none;\n padding: 0;\n background: transparent;\n color: inherit;\n}\n\ndialog::backdrop {\n background: transparent;\n}\n\n.toggle-button {\n display: block;\n position: fixed;\n left: 0;\n bottom: 0;\n color: white;\n background: black;\n padding: 12px 16px;\n border-radius: 0 10px 0 0;\n opacity: 0;\n transition: opacity 0.6s ease-in-out;\n z-index: 10000;\n font-size: 12px;\n cursor: pointer;\n text-decoration: none;\n}\n\n.toggle-button:hover {\n opacity: 1;\n}\n\n@media screen and (max-width: 480px) {\n .toggle-button {\n display: none;\n }\n}\n", e.append(t), this.dialog = document.createElement("dialog");
125
+ let n = document.createElement("a");
126
+ n.classList.add("toggle-button"), n.textContent = "Open CopyTuner", n.addEventListener("click", () => this.#e()), e.append(this.dialog, n);
127
+ }
128
+ set onToggle(e) {
129
+ this.#e = e;
130
+ }
138
131
  };
139
- customElements.define("copytuner-bar", s), customElements.define("copyray-overlay", o);
140
- var c = () => {
141
- let { url: t, data: n, keysSkipped: r } = window.CopyTuner, i = (e) => window.open(`${t}/blurbs/${e}/edit`), a = document.createElement("copytuner-bar");
142
- document.body.append(a), a.init({
143
- url: t,
144
- data: n,
132
+ customElements.define("copytuner-bar", i), customElements.define("copyray-overlay", t), customElements.define("copytuner-root", a);
133
+ var o = () => {
134
+ let { url: e, data: t, keysSkipped: r } = window.CopyTuner, i = (t) => window.open(`${e}/blurbs/${t}/edit`), a = document.createElement("copytuner-root");
135
+ document.body.append(a);
136
+ let o = document.createElement("copytuner-bar"), s = document.createElement("copyray-overlay");
137
+ s.onOpen = i, a.dialog.append(s, o), o.init({
138
+ url: e,
139
+ data: t,
145
140
  keysSkipped: !!r,
146
141
  onOpen: i
147
142
  });
148
- let o = document.createElement("copyray-overlay");
149
- o.onOpen = i, document.body.append(o);
150
- let s = () => {
151
- o.show(), a.show();
152
- }, c = () => {
153
- o.hide(), a.hide();
154
- }, l = () => o.isShowing ? c() : s();
155
- o.onToggle = l, window.CopyTuner.toggle = l, document.addEventListener("keydown", (t) => {
156
- if (o.isShowing && ["Escape", "Esc"].includes(t.key)) {
157
- c();
158
- return;
159
- }
160
- (e && t.metaKey || !e && t.ctrlKey) && t.shiftKey && t.key.toLowerCase() === "k" && l();
161
- }), console && console.log(`Ready to Copyray. Press ${e ? "cmd+shift+k" : "ctrl+shift+k"} to scan your UI.`);
143
+ let c = () => {
144
+ a.dialog.showModal(), s.show(), o.show();
145
+ }, l = () => {
146
+ a.dialog.close(), s.hide();
147
+ }, u = () => a.dialog.open ? l() : c();
148
+ a.onToggle = u, s.onClose = l, window.CopyTuner.toggle = u, a.dialog.addEventListener("close", () => {
149
+ a.dialog.open || s.hide();
150
+ }), document.addEventListener("keydown", (e) => {
151
+ (n && e.metaKey || !n && e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "k" && u();
152
+ }), console && console.log(`Ready to Copyray. Press ${n ? "cmd+shift+k" : "ctrl+shift+k"} to scan your UI.`);
162
153
  };
163
- document.readyState === "complete" || document.readyState !== "loading" ? c() : document.addEventListener("DOMContentLoaded", () => c());
154
+ document.readyState === "complete" || document.readyState !== "loading" ? o() : document.addEventListener("DOMContentLoaded", () => o());
164
155
  //#endregion
data/index.html CHANGED
@@ -6,6 +6,35 @@
6
6
  <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
  <title>CopyTuner Dev</title>
9
+ <style>
10
+ .fixed-banner {
11
+ position: fixed;
12
+ top: 0;
13
+ right: 0;
14
+ padding: 8px 12px;
15
+ background: #ffe9a8;
16
+ }
17
+
18
+ /* width は横スクロール時の left 経路の検証用 */
19
+ .spacer {
20
+ height: 150vh;
21
+ width: 150vw;
22
+ }
23
+
24
+ /* transform を持つ祖先は fixed 要素の containing block になり、viewport 基準からずれるケースの再現用 */
25
+ .transform-ancestor {
26
+ transform: translateZ(0);
27
+ height: 80px;
28
+ }
29
+
30
+ .transformed-fixed {
31
+ position: fixed;
32
+ bottom: 0;
33
+ left: 0;
34
+ padding: 8px 12px;
35
+ background: #a8d8ff;
36
+ }
37
+ </style>
9
38
  </head>
10
39
 
11
40
  <body>
@@ -15,6 +44,23 @@
15
44
  <li data-copyray-key="projects.index.blurbs">翻訳キー</li>
16
45
  <li data-copyray-key="projects.index.members">メンバー数</li>
17
46
  </ul>
47
+
48
+ <p class="fixed-banner" data-copyray-key="projects.index.fixed_banner">固定表示のバナー</p>
49
+
50
+ <div class="transform-ancestor">
51
+ <p class="transformed-fixed" data-copyray-key="projects.index.transformed_fixed">transform 祖先を持つ固定要素</p>
52
+ </div>
53
+
54
+ <button type="button" id="open-app-modal">アプリのモーダルを開く</button>
55
+
56
+ <dialog id="app-modal">
57
+ <p data-copyray-key="projects.index.modal_body">モーダル内の文言</p>
58
+ <button type="button" id="close-app-modal">閉じる</button>
59
+ </dialog>
60
+
61
+ <div class="spacer"></div>
62
+
63
+ <p data-copyray-key="projects.index.below_spacer">スクロール後に見える文言</p>
18
64
  </div>
19
65
  <script>
20
66
  window.CopyTuner = {
@@ -23,8 +69,16 @@
23
69
  'projects.index.locales': '言語',
24
70
  'projects.index.blurbs': '翻訳キー',
25
71
  'projects.index.members': 'メンバー数',
72
+ 'projects.index.fixed_banner': '固定表示のバナー',
73
+ 'projects.index.transformed_fixed': 'transform 祖先を持つ固定要素',
74
+ 'projects.index.modal_body': 'モーダル内の文言',
75
+ 'projects.index.below_spacer': 'スクロール後に見える文言',
26
76
  },
27
77
  }
78
+
79
+ const appModal = document.getElementById('app-modal')
80
+ document.getElementById('open-app-modal').addEventListener('click', () => appModal.showModal())
81
+ document.getElementById('close-app-modal').addEventListener('click', () => appModal.close())
28
82
  </script>
29
83
  <script type="module" src="/src/main.ts"></script>
30
84
  </body>
@@ -1,6 +1,6 @@
1
1
  module CopyTunerClient
2
2
  # Client version
3
- VERSION = '2.3.0'.freeze
3
+ VERSION = '2.4.0'.freeze
4
4
 
5
5
  # API version being used to communicate with the server
6
6
  API_VERSION = '2.0'.freeze
@@ -1,5 +1,4 @@
1
1
  import { OVERLAY_STYLES } from './styles'
2
- import { computeBoundingBox } from './util'
3
2
 
4
3
  type OpenCallback = (key: string) => void
5
4
 
@@ -15,13 +14,14 @@ const findBlurbs = (): Blurb[] =>
15
14
  element,
16
15
  }))
17
16
 
18
- // オーバーレイ背景・翻訳要素のハイライト枠(specimen)・トグルボタンをまとめて Shadow DOM 内に描画する。
17
+ // オーバーレイ背景と翻訳要素のハイライト枠(specimen)を Shadow DOM 内に描画する。
19
18
  export class CopyrayOverlay extends HTMLElement {
20
19
  #onOpen: OpenCallback = () => {}
21
- #onToggle: () => void = () => {}
20
+ #onClose: () => void = () => {}
22
21
  #backdrop: HTMLDivElement
23
22
  #specimens: HTMLDivElement
24
- #toggleButton: HTMLAnchorElement
23
+ #boxes = new Map<Element, HTMLDivElement>()
24
+ #frame: number | null = null
25
25
 
26
26
  constructor() {
27
27
  super()
@@ -33,74 +33,81 @@ export class CopyrayOverlay extends HTMLElement {
33
33
 
34
34
  this.#backdrop = document.createElement('div')
35
35
  this.#backdrop.classList.add('backdrop')
36
- this.#backdrop.addEventListener('click', () => this.hide())
36
+ this.#backdrop.addEventListener('click', () => this.#onClose())
37
37
 
38
- // specimen をページ座標基準で absolute 配置するコンテナ
39
38
  this.#specimens = document.createElement('div')
40
- this.#specimens.classList.add('specimens')
41
39
 
42
- this.#toggleButton = document.createElement('a')
43
- this.#toggleButton.classList.add('toggle-button')
44
- this.#toggleButton.textContent = 'Open CopyTuner'
45
- // 旧実装ではトグルボタンが overlay と bar の両方を表示していた。show() ではなく onToggle 経由で表示する。
46
- this.#toggleButton.addEventListener('click', () => this.#onToggle())
47
-
48
- shadow.append(this.#backdrop, this.#specimens, this.#toggleButton)
49
-
50
- // 初期は非表示(背景と specimen を隠す)。トグルボタンは常時表示のため :host([hidden]) は使わず個別制御する。
51
- this.hide()
40
+ shadow.append(this.#backdrop, this.#specimens)
52
41
  }
53
42
 
54
43
  set onOpen(callback: OpenCallback) {
55
44
  this.#onOpen = callback
56
45
  }
57
46
 
58
- set onToggle(callback: () => void) {
59
- this.#onToggle = callback
60
- }
61
-
62
- get isShowing(): boolean {
63
- return !this.#backdrop.hidden
47
+ set onClose(callback: () => void) {
48
+ this.#onClose = callback
64
49
  }
65
50
 
66
51
  show() {
67
- this.reset()
68
- this.#backdrop.hidden = false
52
+ // 二重 show でもリスナーと boxes が重複しないよう、まず後始末を通す
53
+ this.hide()
69
54
 
70
55
  for (const { element, keys } of findBlurbs()) {
71
- const box = this.makeBox(element, keys)
72
- if (box) {
73
- this.#specimens.append(box)
74
- }
56
+ this.#boxes.set(element, this.makeBox(keys))
75
57
  }
58
+ // 初回は同期で配置する(rAF 経由だと開いた直後の 1 フレームだけ位置がずれる)
59
+ this.#reposition()
60
+ this.#specimens.append(...this.#boxes.values())
61
+
62
+ document.addEventListener('scroll', this.#requestReposition, { capture: true, passive: true })
63
+ window.addEventListener('resize', this.#requestReposition, { passive: true })
76
64
  }
77
65
 
78
66
  hide() {
79
- this.reset()
80
- this.#backdrop.hidden = true
81
- }
67
+ // capture フラグはリスナーの同一性判定に含まれるため、省くと解除されない
68
+ document.removeEventListener('scroll', this.#requestReposition, { capture: true })
69
+ window.removeEventListener('resize', this.#requestReposition)
70
+
71
+ // 閉じた後に走っても boxes は空で no-op になるが、無駄なコールバックを残さない
72
+ if (this.#frame !== null) {
73
+ cancelAnimationFrame(this.#frame)
74
+ this.#frame = null
75
+ }
82
76
 
83
- reset() {
77
+ // detached な box を握り続けると開くたびに肥大する
78
+ this.#boxes.clear()
84
79
  this.#specimens.replaceChildren()
85
80
  }
86
81
 
87
- private makeBox(element: Element, keys: string[]): HTMLDivElement | null {
88
- const bounds = computeBoundingBox(element)
89
- if (bounds === null) return null
82
+ // scroll は慣性スクロール中に毎フレーム相当で発火するため、同一フレーム内の再計測は 1 回にまとめる
83
+ #requestReposition = () => {
84
+ if (this.#frame !== null) return
90
85
 
86
+ this.#frame = requestAnimationFrame(() => {
87
+ this.#frame = null
88
+ this.#reposition()
89
+ })
90
+ }
91
+
92
+ #reposition() {
93
+ // 読みと書きを分けないと、要素数分の強制同期レイアウトが走る
94
+ const measured = Array.from(this.#boxes, ([element, box]) => [box, element.getBoundingClientRect()] as const)
95
+
96
+ for (const [box, rect] of measured) {
97
+ // display: none / DOM から外れた要素は rect が全て 0 になる。原点に枠だけ残さないよう隠す
98
+ box.hidden = rect.width === 0 && rect.height === 0
99
+ if (box.hidden) continue
100
+
101
+ box.style.left = `${rect.left}px`
102
+ box.style.top = `${rect.top}px`
103
+ box.style.width = `${rect.width}px`
104
+ box.style.height = `${rect.height}px`
105
+ }
106
+ }
107
+
108
+ private makeBox(keys: string[]): HTMLDivElement {
91
109
  const box = document.createElement('div')
92
110
  box.classList.add('specimen')
93
- box.style.left = `${bounds.left}px`
94
- box.style.top = `${bounds.top}px`
95
- box.style.width = `${bounds.width}px`
96
- box.style.height = `${bounds.height}px`
97
-
98
- const { position, top, left } = getComputedStyle(element)
99
- if (position === 'fixed') {
100
- box.style.position = 'fixed'
101
- box.style.top = top
102
- box.style.left = left
103
- }
104
111
 
105
112
  // box 全体のクリックは先頭キーを開く(広いクリック領域を維持)。複数キー時は各ラベルから個別に開ける
106
113
  box.addEventListener('click', () => this.#onOpen(keys[0]))
data/src/copytuner-bar.ts CHANGED
@@ -21,12 +21,6 @@ export class CopytunerBar extends HTMLElement {
21
21
  this.attachShadow({ mode: 'open' })
22
22
  }
23
23
 
24
- // custom element の constructor 内では属性・プロパティを変更できない(createElement が弾く)ため、
25
- // hidden の初期化は DOM 挿入後に呼ばれる connectedCallback で行う。
26
- connectedCallback() {
27
- this.hidden = true
28
- }
29
-
30
24
  // url/data/keysSkipped/onOpen はオブジェクトや関数を含むため属性ではなくメソッドで受け渡す。
31
25
  init({ url, data, keysSkipped, onOpen }: InitOptions) {
32
26
  this.#onOpen = onOpen
@@ -66,14 +60,9 @@ export class CopytunerBar extends HTMLElement {
66
60
  }
67
61
 
68
62
  show() {
69
- this.hidden = false
70
63
  this.#searchBox.focus()
71
64
  }
72
65
 
73
- hide() {
74
- this.hidden = true
75
- }
76
-
77
66
  private makeButton(label: string, href: string, target?: string): HTMLAnchorElement {
78
67
  const button = document.createElement('a')
79
68
  button.classList.add('button')
@@ -0,0 +1,31 @@
1
+ import { ROOT_STYLES } from './styles'
2
+
3
+ // dialog を showModal() で top layer の最上位に載せることで、アプリ側のモーダル dialog による
4
+ // inert 化を受けずに操作できる。トグルボタンだけ dialog の外に置くのは、
5
+ // 閉じている間 dialog のサブツリーが display: none になり、常時表示のボタンが消えてしまうため。
6
+ export class CopytunerRoot extends HTMLElement {
7
+ readonly dialog: HTMLDialogElement
8
+ #onToggle: () => void = () => {}
9
+
10
+ constructor() {
11
+ super()
12
+ const shadow = this.attachShadow({ mode: 'open' })
13
+
14
+ const style = document.createElement('style')
15
+ style.textContent = ROOT_STYLES
16
+ shadow.append(style)
17
+
18
+ this.dialog = document.createElement('dialog')
19
+
20
+ const toggleButton = document.createElement('a')
21
+ toggleButton.classList.add('toggle-button')
22
+ toggleButton.textContent = 'Open CopyTuner'
23
+ toggleButton.addEventListener('click', () => this.#onToggle())
24
+
25
+ shadow.append(this.dialog, toggleButton)
26
+ }
27
+
28
+ set onToggle(callback: () => void) {
29
+ this.#onToggle = callback
30
+ }
31
+ }
data/src/main.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable no-console */
2
2
  import { CopyrayOverlay } from './copyray-overlay'
3
3
  import { CopytunerBar } from './copytuner-bar'
4
+ import { CopytunerRoot } from './copytuner-root'
4
5
  import { isMac } from './util'
5
6
 
6
7
  declare global {
@@ -18,38 +19,47 @@ declare global {
18
19
 
19
20
  customElements.define('copytuner-bar', CopytunerBar)
20
21
  customElements.define('copyray-overlay', CopyrayOverlay)
22
+ customElements.define('copytuner-root', CopytunerRoot)
21
23
 
22
24
  const start = () => {
23
25
  const { url, data, keysSkipped } = window.CopyTuner
24
26
  const onOpen = (key: string) => window.open(`${url}/blurbs/${key}/edit`)
25
27
 
26
- const bar = document.createElement('copytuner-bar') as CopytunerBar
27
- document.body.append(bar)
28
- bar.init({ url, data, keysSkipped: Boolean(keysSkipped), onOpen })
28
+ const root = document.createElement('copytuner-root') as CopytunerRoot
29
+ document.body.append(root)
29
30
 
31
+ const bar = document.createElement('copytuner-bar') as CopytunerBar
30
32
  const overlay = document.createElement('copyray-overlay') as CopyrayOverlay
31
33
  overlay.onOpen = onOpen
32
- document.body.append(overlay)
34
+ root.dialog.append(overlay, bar)
35
+ bar.init({ url, data, keysSkipped: Boolean(keysSkipped), onOpen })
33
36
 
34
37
  const show = () => {
38
+ // display: none のままでは searchBox にフォーカスが当たらないので dialog を先に開く
39
+ root.dialog.showModal()
35
40
  overlay.show()
36
41
  bar.show()
37
42
  }
38
43
  const hide = () => {
44
+ root.dialog.close()
45
+ // close イベントを待たず同期で後始末する。イベントは queued task 発火で、
46
+ // 自動テスト環境など発火が遅れる/落ちる状況があるため、ここを唯一の頼りにしない
39
47
  overlay.hide()
40
- bar.hide()
41
48
  }
42
- const toggle = () => (overlay.isShowing ? hide() : show())
49
+ const toggle = () => (root.dialog.open ? hide() : show())
43
50
 
44
- overlay.onToggle = toggle
51
+ root.onToggle = toggle
52
+ overlay.onClose = hide
45
53
  window.CopyTuner.toggle = toggle
46
54
 
47
- document.addEventListener('keydown', (event) => {
48
- if (overlay.isShowing && ['Escape', 'Esc'].includes(event.key)) {
49
- hide()
50
- return
51
- }
55
+ // Escape のようにこちらの hide() を経由しない閉じ方の受け皿。overlay.hide() は冪等なので二重でも問題ない
56
+ root.dialog.addEventListener('close', () => {
57
+ // 閉じた直後に開き直された場合、遅れて届いたこのイベントで後始末してはいけない
58
+ if (root.dialog.open) return
59
+ overlay.hide()
60
+ })
52
61
 
62
+ document.addEventListener('keydown', (event) => {
53
63
  if (((isMac && event.metaKey) || (!isMac && event.ctrlKey)) && event.shiftKey && event.key.toLowerCase() === 'k') {
54
64
  toggle()
55
65
  }
data/src/styles.ts CHANGED
@@ -1,6 +1,57 @@
1
1
  // 各 custom element の Shadow root に <style> として注入する CSS。
2
2
  // Shadow DOM のスタイル隔離が効くため、旧 copyray.css にあった #copyray-overlay * のグローバルリセットは不要。
3
3
 
4
+ // コンテナ(<copytuner-root>)のスタイル。
5
+ // dialog の UA デフォルト(中央寄せ・白背景・枠線・padding・暗転)を全て打ち消し、
6
+ // viewport 全面を覆う透明なコンテナにする。暗転は overlay 側の .backdrop が担う。
7
+ export const ROOT_STYLES = `
8
+ /* transform / filter / perspective / contain は置かない(理由は OVERLAY_STYLES のコメント参照) */
9
+ dialog {
10
+ position: fixed;
11
+ inset: 0;
12
+ width: 100%;
13
+ height: 100%;
14
+ max-width: none;
15
+ max-height: none;
16
+ margin: 0;
17
+ border: none;
18
+ padding: 0;
19
+ background: transparent;
20
+ color: inherit;
21
+ }
22
+
23
+ dialog::backdrop {
24
+ background: transparent;
25
+ }
26
+
27
+ .toggle-button {
28
+ display: block;
29
+ position: fixed;
30
+ left: 0;
31
+ bottom: 0;
32
+ color: white;
33
+ background: black;
34
+ padding: 12px 16px;
35
+ border-radius: 0 10px 0 0;
36
+ opacity: 0;
37
+ transition: opacity 0.6s ease-in-out;
38
+ z-index: 10000;
39
+ font-size: 12px;
40
+ cursor: pointer;
41
+ text-decoration: none;
42
+ }
43
+
44
+ .toggle-button:hover {
45
+ opacity: 1;
46
+ }
47
+
48
+ @media screen and (max-width: 480px) {
49
+ .toggle-button {
50
+ display: none;
51
+ }
52
+ }
53
+ `
54
+
4
55
  // ツールバー(<copytuner-bar>)のスタイル。:host にバー本体のレイアウトを定義する。
5
56
  export const BAR_STYLES = `
6
57
  :host {
@@ -20,10 +71,6 @@ export const BAR_STYLES = `
20
71
  box-sizing: border-box;
21
72
  }
22
73
 
23
- :host([hidden]) {
24
- display: none;
25
- }
26
-
27
74
  .log-menu {
28
75
  position: fixed;
29
76
  left: 0;
@@ -109,22 +156,19 @@ export const BAR_STYLES = `
109
156
  `
110
157
 
111
158
  // オーバーレイ(<copyray-overlay>)のスタイル。
112
- // :host はドキュメント原点基準(position: absolute; top/left: 0)にして、
113
- // 子の specimen を computeBoundingBox のページ座標で absolute 配置できるようにする。
114
- // 背景の暗転(.backdrop)だけは viewport 固定(fixed)にする。
159
+ // specimen は getBoundingClientRect() の viewport 座標をそのまま使うため、
160
+ // dialog / :host / specimen のコンテナ div のいずれにも
161
+ // transform / filter / perspective / contain を置かない。
162
+ // 置くとそれが containing block になり、fixed な specimen が viewport 基準から外れる。
163
+ // :host の position: absolute と width/height: 0 は、インラインの custom element が
164
+ // dialog のフローに line box を作らないようにするため(fixed の containing block は作らない)。
115
165
  export const OVERLAY_STYLES = `
116
166
  :host {
117
167
  position: absolute;
118
- top: 0;
119
- left: 0;
120
168
  width: 0;
121
169
  height: 0;
122
170
  }
123
171
 
124
- :host([hidden]) {
125
- display: none;
126
- }
127
-
128
172
  .backdrop {
129
173
  position: fixed;
130
174
  inset: 0;
@@ -137,7 +181,7 @@ export const OVERLAY_STYLES = `
137
181
  }
138
182
 
139
183
  .specimen {
140
- position: absolute;
184
+ position: fixed;
141
185
  background: rgba(255, 50, 50, 0.1);
142
186
  outline: 1px solid rgba(255, 50, 50, 0.8);
143
187
  outline-offset: -1px;
@@ -153,6 +197,10 @@ export const OVERLAY_STYLES = `
153
197
  background: rgba(255, 50, 50, 0.4);
154
198
  }
155
199
 
200
+ .specimen[hidden] {
201
+ display: none;
202
+ }
203
+
156
204
  .specimen-handle {
157
205
  float: left;
158
206
  margin: 0 2px 2px 0;
@@ -162,31 +210,4 @@ export const OVERLAY_STYLES = `
162
210
  font-size: 10px;
163
211
  cursor: pointer;
164
212
  }
165
-
166
- .toggle-button {
167
- display: block;
168
- position: fixed;
169
- left: 0;
170
- bottom: 0;
171
- color: white;
172
- background: black;
173
- padding: 12px 16px;
174
- border-radius: 0 10px 0 0;
175
- opacity: 0;
176
- transition: opacity 0.6s ease-in-out;
177
- z-index: 10000;
178
- font-size: 12px;
179
- cursor: pointer;
180
- text-decoration: none;
181
- }
182
-
183
- .toggle-button:hover {
184
- opacity: 1;
185
- }
186
-
187
- @media screen and (max-width: 480px) {
188
- .toggle-button {
189
- display: none;
190
- }
191
- }
192
213
  `
data/src/util.ts CHANGED
@@ -1,40 +1,5 @@
1
1
  const isMac = navigator.platform.toUpperCase().includes('MAC')
2
2
 
3
- // @ts-expect-error TS7006
4
- const isVisible = (element) => !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length > 0)
5
-
6
- // @ts-expect-error TS7006
7
- const getOffset = (elment) => {
8
- const box = elment.getBoundingClientRect()
9
-
10
- return {
11
- top: box.top + (window.pageYOffset - document.documentElement.clientTop),
12
- left: box.left + (window.pageXOffset - document.documentElement.clientLeft),
13
- }
14
- }
15
-
16
- // @ts-expect-error TS7006
17
- const computeBoundingBox = (element) => {
18
- if (!isVisible(element)) {
19
- return null
20
- }
21
-
22
- const boxFrame = getOffset(element)
23
- // @ts-expect-error TS2339
24
- boxFrame.right = boxFrame.left + element.offsetWidth
25
- // @ts-expect-error TS2339
26
- boxFrame.bottom = boxFrame.top + element.offsetHeight
27
-
28
- return {
29
- left: boxFrame.left,
30
- top: boxFrame.top,
31
- // @ts-expect-error TS2339
32
- width: boxFrame.right - boxFrame.left,
33
- // @ts-expect-error TS2339
34
- height: boxFrame.bottom - boxFrame.top,
35
- }
36
- }
37
-
38
3
  const debounce = <A extends unknown[]>(fn: (...args: A) => void, wait: number) => {
39
4
  let timer: ReturnType<typeof setTimeout> | undefined
40
5
  return (...args: A) => {
@@ -43,4 +8,4 @@ const debounce = <A extends unknown[]>(fn: (...args: A) => void, wait: number) =
43
8
  }
44
9
  }
45
10
 
46
- export { computeBoundingBox, debounce, getOffset, isMac, isVisible }
11
+ export { debounce, isMac }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: copy_tuner_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SonicGarden
@@ -277,6 +277,7 @@ files:
277
277
  - spec/support/writing_cache.rb
278
278
  - src/copyray-overlay.ts
279
279
  - src/copytuner-bar.ts
280
+ - src/copytuner-root.ts
280
281
  - src/main.ts
281
282
  - src/styles.ts
282
283
  - src/util.ts
@@ -306,7 +307,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
306
307
  - !ruby/object:Gem::Version
307
308
  version: '0'
308
309
  requirements: []
309
- rubygems_version: 4.0.15
310
+ rubygems_version: 4.0.10
310
311
  specification_version: 4
311
312
  summary: Client for the CopyTuner copy management service
312
313
  test_files: []