copy_tuner_client 2.0.0 → 2.1.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/CLAUDE.md +4 -1
- data/README.md +6 -2
- data/app/assets/javascripts/copytuner.js +162 -255
- data/biome.json +39 -0
- data/index.html +9 -11
- data/lib/copy_tuner_client/copyray_middleware.rb +0 -6
- data/lib/copy_tuner_client/engine.rb +1 -1
- data/lib/copy_tuner_client/version.rb +1 -1
- data/mise.toml +3 -0
- data/package.json +9 -13
- data/pnpm-lock.yaml +600 -0
- data/spec/copy_tuner_client/copyray_middleware_spec.rb +1 -2
- data/src/copyray-overlay.ts +125 -0
- data/src/copytuner-bar.ts +153 -0
- data/src/main.ts +29 -25
- data/src/{copyray.css → styles.ts} +104 -139
- data/src/util.ts +9 -1
- data/tsconfig.json +5 -10
- data/vite.config.ts +0 -1
- metadata +7 -8
- data/.eslintrc.js +0 -12
- data/app/assets/stylesheets/copytuner.css +0 -1
- data/src/copyray.ts +0 -111
- data/src/copytuner_bar.ts +0 -129
- data/src/specimen.ts +0 -94
- data/yarn.lock +0 -2540
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { OVERLAY_STYLES } from './styles'
|
|
2
|
+
import { computeBoundingBox } from './util'
|
|
3
|
+
|
|
4
|
+
type OpenCallback = (key: string) => void
|
|
5
|
+
|
|
6
|
+
type Blurb = {
|
|
7
|
+
keys: string[]
|
|
8
|
+
element: Element
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const findBlurbs = (): Blurb[] =>
|
|
12
|
+
Array.from(document.querySelectorAll('[data-copyray-key]')).map((element) => ({
|
|
13
|
+
// 1 要素に複数キーがカンマ区切りで入りうる(同一テキストノードに複数訳文が連結された場合)
|
|
14
|
+
keys: (element.getAttribute('data-copyray-key') ?? '').split(',').filter(Boolean),
|
|
15
|
+
element,
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
// オーバーレイ背景・翻訳要素のハイライト枠(specimen)・トグルボタンをまとめて Shadow DOM 内に描画する。
|
|
19
|
+
export class CopyrayOverlay extends HTMLElement {
|
|
20
|
+
#onOpen: OpenCallback = () => {}
|
|
21
|
+
#onToggle: () => void = () => {}
|
|
22
|
+
#backdrop: HTMLDivElement
|
|
23
|
+
#specimens: HTMLDivElement
|
|
24
|
+
#toggleButton: HTMLAnchorElement
|
|
25
|
+
|
|
26
|
+
constructor() {
|
|
27
|
+
super()
|
|
28
|
+
const shadow = this.attachShadow({ mode: 'open' })
|
|
29
|
+
|
|
30
|
+
const style = document.createElement('style')
|
|
31
|
+
style.textContent = OVERLAY_STYLES
|
|
32
|
+
shadow.append(style)
|
|
33
|
+
|
|
34
|
+
this.#backdrop = document.createElement('div')
|
|
35
|
+
this.#backdrop.classList.add('backdrop')
|
|
36
|
+
this.#backdrop.addEventListener('click', () => this.hide())
|
|
37
|
+
|
|
38
|
+
// specimen をページ座標基準で absolute 配置するコンテナ
|
|
39
|
+
this.#specimens = document.createElement('div')
|
|
40
|
+
this.#specimens.classList.add('specimens')
|
|
41
|
+
|
|
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()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
set onOpen(callback: OpenCallback) {
|
|
55
|
+
this.#onOpen = callback
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
set onToggle(callback: () => void) {
|
|
59
|
+
this.#onToggle = callback
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get isShowing(): boolean {
|
|
63
|
+
return !this.#backdrop.hidden
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
show() {
|
|
67
|
+
this.reset()
|
|
68
|
+
this.#backdrop.hidden = false
|
|
69
|
+
|
|
70
|
+
for (const { element, keys } of findBlurbs()) {
|
|
71
|
+
const box = this.makeBox(element, keys)
|
|
72
|
+
if (box) {
|
|
73
|
+
this.#specimens.append(box)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
hide() {
|
|
79
|
+
this.reset()
|
|
80
|
+
this.#backdrop.hidden = true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
reset() {
|
|
84
|
+
this.#specimens.replaceChildren()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private makeBox(element: Element, keys: string[]): HTMLDivElement | null {
|
|
88
|
+
const bounds = computeBoundingBox(element)
|
|
89
|
+
if (bounds === null) return null
|
|
90
|
+
|
|
91
|
+
const box = document.createElement('div')
|
|
92
|
+
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
|
+
|
|
105
|
+
// box 全体のクリックは先頭キーを開く(広いクリック領域を維持)。複数キー時は各ラベルから個別に開ける
|
|
106
|
+
box.addEventListener('click', () => this.#onOpen(keys[0]))
|
|
107
|
+
|
|
108
|
+
for (const key of keys) {
|
|
109
|
+
box.append(this.makeLabel(key))
|
|
110
|
+
}
|
|
111
|
+
return box
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private makeLabel(key: string): HTMLDivElement {
|
|
115
|
+
const label = document.createElement('div')
|
|
116
|
+
label.classList.add('specimen-handle')
|
|
117
|
+
label.textContent = key
|
|
118
|
+
// ラベルのクリックはそのキーを開く。box への伝播を止めて先頭キーとの二重発火を防ぐ
|
|
119
|
+
label.addEventListener('click', (event) => {
|
|
120
|
+
event.stopPropagation()
|
|
121
|
+
this.#onOpen(key)
|
|
122
|
+
})
|
|
123
|
+
return label
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { BAR_STYLES } from './styles'
|
|
2
|
+
import { debounce } from './util'
|
|
3
|
+
|
|
4
|
+
type OpenCallback = (key: string) => void
|
|
5
|
+
|
|
6
|
+
type InitOptions = {
|
|
7
|
+
url: string
|
|
8
|
+
data: Record<string, string>
|
|
9
|
+
keysSkipped: boolean
|
|
10
|
+
onOpen: OpenCallback
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// 画面下部のツールバー。CopyTuner / Sync ボタン、ページ内翻訳の検索・ログメニューを Shadow DOM 内に描画する。
|
|
14
|
+
export class CopytunerBar extends HTMLElement {
|
|
15
|
+
#onOpen: OpenCallback = () => {}
|
|
16
|
+
#searchBox!: HTMLInputElement
|
|
17
|
+
#logMenu!: HTMLDivElement
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
super()
|
|
21
|
+
this.attachShadow({ mode: 'open' })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// custom element の constructor 内では属性・プロパティを変更できない(createElement が弾く)ため、
|
|
25
|
+
// hidden の初期化は DOM 挿入後に呼ばれる connectedCallback で行う。
|
|
26
|
+
connectedCallback() {
|
|
27
|
+
this.hidden = true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// url/data/keysSkipped/onOpen はオブジェクトや関数を含むため属性ではなくメソッドで受け渡す。
|
|
31
|
+
init({ url, data, keysSkipped, onOpen }: InitOptions) {
|
|
32
|
+
this.#onOpen = onOpen
|
|
33
|
+
const shadow = this.shadowRoot as ShadowRoot
|
|
34
|
+
|
|
35
|
+
const style = document.createElement('style')
|
|
36
|
+
style.textContent = BAR_STYLES
|
|
37
|
+
shadow.append(style)
|
|
38
|
+
|
|
39
|
+
// 元々 Rails から出力されていたマークアップに合わせたボタン群。
|
|
40
|
+
// url は設定値だが innerHTML に直接埋めず setAttribute で渡す(XSS 面で安全側に倒す)。
|
|
41
|
+
const copyTunerButton = this.makeButton('CopyTuner', url, '_blank')
|
|
42
|
+
const syncButton = this.makeButton('Sync', '/copytuner', '_blank')
|
|
43
|
+
const openLogButton = this.makeButton('Translations in this page', 'javascript:void(0)')
|
|
44
|
+
|
|
45
|
+
this.#searchBox = document.createElement('input')
|
|
46
|
+
this.#searchBox.type = 'text'
|
|
47
|
+
this.#searchBox.classList.add('search')
|
|
48
|
+
this.#searchBox.placeholder = 'search'
|
|
49
|
+
|
|
50
|
+
shadow.append(copyTunerButton, syncButton, openLogButton, this.#searchBox)
|
|
51
|
+
|
|
52
|
+
this.#logMenu = this.makeLogMenu(data)
|
|
53
|
+
shadow.append(this.#logMenu)
|
|
54
|
+
|
|
55
|
+
// 巨大DOM/Nokogiri例外でキー付与がスキップされた場合は、オーバーレイが使えないので
|
|
56
|
+
// ツールバー(Translations in this page)から編集する旨を案内する。
|
|
57
|
+
if (keysSkipped) {
|
|
58
|
+
this.appendSkippedNotice()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
openLogButton.addEventListener('click', (event) => {
|
|
62
|
+
event.preventDefault()
|
|
63
|
+
this.toggleLogMenu()
|
|
64
|
+
})
|
|
65
|
+
this.#searchBox.addEventListener('input', debounce(this.onSearch.bind(this), 250))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
show() {
|
|
69
|
+
this.hidden = false
|
|
70
|
+
this.#searchBox.focus()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
hide() {
|
|
74
|
+
this.hidden = true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private makeButton(label: string, href: string, target?: string): HTMLAnchorElement {
|
|
78
|
+
const button = document.createElement('a')
|
|
79
|
+
button.classList.add('button')
|
|
80
|
+
button.textContent = label
|
|
81
|
+
button.href = href
|
|
82
|
+
if (target) {
|
|
83
|
+
button.target = target
|
|
84
|
+
}
|
|
85
|
+
return button
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private appendSkippedNotice() {
|
|
89
|
+
const notice = document.createElement('span')
|
|
90
|
+
notice.classList.add('notice')
|
|
91
|
+
notice.textContent = '⚠ This page is too large for the overlay. Use "Translations in this page" to edit.'
|
|
92
|
+
;(this.shadowRoot as ShadowRoot).append(notice)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private showLogMenu() {
|
|
96
|
+
this.#logMenu.hidden = false
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private toggleLogMenu() {
|
|
100
|
+
this.#logMenu.hidden = !this.#logMenu.hidden
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private makeLogMenu(data: Record<string, string>): HTMLDivElement {
|
|
104
|
+
const div = document.createElement('div')
|
|
105
|
+
div.classList.add('log-menu')
|
|
106
|
+
div.hidden = true
|
|
107
|
+
|
|
108
|
+
const table = document.createElement('table')
|
|
109
|
+
const tbody = document.createElement('tbody')
|
|
110
|
+
|
|
111
|
+
for (const key of Object.keys(data).sort()) {
|
|
112
|
+
const value = data[key]
|
|
113
|
+
if (value === '') {
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const td1 = document.createElement('td')
|
|
118
|
+
td1.textContent = key
|
|
119
|
+
const td2 = document.createElement('td')
|
|
120
|
+
td2.textContent = value
|
|
121
|
+
const tr = document.createElement('tr')
|
|
122
|
+
tr.dataset.key = key
|
|
123
|
+
|
|
124
|
+
tr.addEventListener('click', ({ currentTarget }) => {
|
|
125
|
+
const row = currentTarget as HTMLTableRowElement
|
|
126
|
+
if (row.dataset.key) {
|
|
127
|
+
this.#onOpen(row.dataset.key)
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
tr.append(td1, td2)
|
|
132
|
+
tbody.append(tr)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
table.append(tbody)
|
|
136
|
+
div.append(table)
|
|
137
|
+
|
|
138
|
+
return div
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private onSearch() {
|
|
142
|
+
// debounce 経由で遅延実行されると Event.target は null 化されるため、検索ボックスを直接参照する
|
|
143
|
+
const keyword = this.#searchBox.value.trim()
|
|
144
|
+
this.showLogMenu()
|
|
145
|
+
|
|
146
|
+
const rows = [...this.#logMenu.querySelectorAll('tr')]
|
|
147
|
+
for (const row of rows) {
|
|
148
|
+
const isShow =
|
|
149
|
+
keyword === '' || [...row.querySelectorAll('td')].some((td) => (td.textContent ?? '').includes(keyword))
|
|
150
|
+
row.hidden = !isShow
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
data/src/main.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
|
-
import
|
|
2
|
+
import { CopyrayOverlay } from './copyray-overlay'
|
|
3
|
+
import { CopytunerBar } from './copytuner-bar'
|
|
3
4
|
import { isMac } from './util'
|
|
4
5
|
|
|
5
6
|
declare global {
|
|
@@ -7,8 +8,7 @@ declare global {
|
|
|
7
8
|
CopyTuner: {
|
|
8
9
|
url: string
|
|
9
10
|
toggle?: () => void
|
|
10
|
-
|
|
11
|
-
data: object
|
|
11
|
+
data: Record<string, string>
|
|
12
12
|
// 巨大DOM/Nokogiri例外で data-copyray-key 付与をスキップしたか。
|
|
13
13
|
// true のときオーバーレイは使えないのでツールバーから編集する旨を案内する。
|
|
14
14
|
keysSkipped?: boolean
|
|
@@ -16,38 +16,42 @@ declare global {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// NOTE: 元々railsから出力されいてたマークアップに合わせてひとまず、、
|
|
22
|
-
const appendCopyTunerBar = (url: string) => {
|
|
23
|
-
const bar = document.createElement('div')
|
|
24
|
-
bar.id = 'copy-tuner-bar'
|
|
25
|
-
bar.classList.add('copy-tuner-hidden')
|
|
26
|
-
bar.innerHTML = `
|
|
27
|
-
<a class="copy-tuner-bar-button" target="_blank" href="${url}">CopyTuner</a>
|
|
28
|
-
<a href="/copytuner" target="_blank" class="copy-tuner-bar-button">Sync</a>
|
|
29
|
-
<a href="javascript:void(0)" class="copy-tuner-bar-open-log copy-tuner-bar-button js-copy-tuner-bar-open-log">Translations in this page</a>
|
|
30
|
-
<input type="text" class="copy-tuner-bar__search js-copy-tuner-bar-search" placeholder="search">
|
|
31
|
-
`
|
|
32
|
-
document.body.append(bar)
|
|
33
|
-
}
|
|
19
|
+
customElements.define('copytuner-bar', CopytunerBar)
|
|
20
|
+
customElements.define('copyray-overlay', CopyrayOverlay)
|
|
34
21
|
|
|
35
22
|
const start = () => {
|
|
36
23
|
const { url, data, keysSkipped } = window.CopyTuner
|
|
24
|
+
const onOpen = (key: string) => window.open(`${url}/blurbs/${key}/edit`)
|
|
25
|
+
|
|
26
|
+
const bar = document.createElement('copytuner-bar') as CopytunerBar
|
|
27
|
+
document.body.append(bar)
|
|
28
|
+
bar.init({ url, data, keysSkipped: Boolean(keysSkipped), onOpen })
|
|
29
|
+
|
|
30
|
+
const overlay = document.createElement('copyray-overlay') as CopyrayOverlay
|
|
31
|
+
overlay.onOpen = onOpen
|
|
32
|
+
document.body.append(overlay)
|
|
33
|
+
|
|
34
|
+
const show = () => {
|
|
35
|
+
overlay.show()
|
|
36
|
+
bar.show()
|
|
37
|
+
}
|
|
38
|
+
const hide = () => {
|
|
39
|
+
overlay.hide()
|
|
40
|
+
bar.hide()
|
|
41
|
+
}
|
|
42
|
+
const toggle = () => (overlay.isShowing ? hide() : show())
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
window.CopyTuner.toggle = () => copyray.toggle()
|
|
44
|
+
overlay.onToggle = toggle
|
|
45
|
+
window.CopyTuner.toggle = toggle
|
|
41
46
|
|
|
42
47
|
document.addEventListener('keydown', (event) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
copyray.hide()
|
|
48
|
+
if (overlay.isShowing && ['Escape', 'Esc'].includes(event.key)) {
|
|
49
|
+
hide()
|
|
46
50
|
return
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
if (((isMac && event.metaKey) || (!isMac && event.ctrlKey)) && event.shiftKey && event.key.toLowerCase() === 'k') {
|
|
50
|
-
|
|
54
|
+
toggle()
|
|
51
55
|
}
|
|
52
56
|
})
|
|
53
57
|
|
|
@@ -1,118 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/* selector for element and children */
|
|
4
|
-
#copyray-overlay,
|
|
5
|
-
#copyray-overlay *,
|
|
6
|
-
#copyray-overlay a:hover,
|
|
7
|
-
#copyray-overlay a:visited,
|
|
8
|
-
#copyray-overlay a:active {
|
|
9
|
-
background: none;
|
|
10
|
-
border: none;
|
|
11
|
-
bottom: auto;
|
|
12
|
-
clear: none;
|
|
13
|
-
cursor: default;
|
|
14
|
-
float: none;
|
|
15
|
-
font-family: Arial, Helvetica, sans-serif;
|
|
16
|
-
font-size: medium;
|
|
17
|
-
font-style: normal;
|
|
18
|
-
font-weight: normal;
|
|
19
|
-
height: auto;
|
|
20
|
-
left: auto;
|
|
21
|
-
letter-spacing: normal;
|
|
22
|
-
line-height: normal;
|
|
23
|
-
max-height: none;
|
|
24
|
-
max-width: none;
|
|
25
|
-
min-height: 0;
|
|
26
|
-
min-width: 0;
|
|
27
|
-
overflow: visible;
|
|
28
|
-
position: static;
|
|
29
|
-
right: auto;
|
|
30
|
-
text-align: left;
|
|
31
|
-
text-decoration: none;
|
|
32
|
-
text-indent: 0;
|
|
33
|
-
text-transform: none;
|
|
34
|
-
top: auto;
|
|
35
|
-
visibility: visible;
|
|
36
|
-
white-space: normal;
|
|
37
|
-
width: auto;
|
|
38
|
-
z-index: auto;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
#copyray-overlay {
|
|
42
|
-
position: fixed;
|
|
43
|
-
left: 0;
|
|
44
|
-
top: 0;
|
|
45
|
-
bottom: 0;
|
|
46
|
-
right: 0;
|
|
47
|
-
background-image: radial-gradient(
|
|
48
|
-
ellipse farthest-corner at center,
|
|
49
|
-
rgba(0, 0, 0, 0.4) 10%,
|
|
50
|
-
rgba(0, 0, 0, 0.8) 100%
|
|
51
|
-
);
|
|
52
|
-
z-index: 9000;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.copyray-specimen {
|
|
56
|
-
position: absolute;
|
|
57
|
-
background: rgba(255, 255, 255, 0.15);
|
|
58
|
-
outline: 1px solid rgba(255, 255, 255, 0.8);
|
|
59
|
-
outline-offset: -1px;
|
|
60
|
-
color: #666;
|
|
61
|
-
font-family: 'Helvetica Neue', sans-serif;
|
|
62
|
-
font-size: 13px;
|
|
63
|
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.copyray-specimen:hover {
|
|
67
|
-
cursor: pointer;
|
|
68
|
-
background: rgba(255, 255, 255, 0.4);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
.copyray-specimen.Specimen {
|
|
72
|
-
outline: 1px solid rgba(255, 50, 50, 0.8);
|
|
73
|
-
background: rgba(255, 50, 50, 0.1);
|
|
74
|
-
}
|
|
1
|
+
// 各 custom element の Shadow root に <style> として注入する CSS。
|
|
2
|
+
// Shadow DOM のスタイル隔離が効くため、旧 copyray.css にあった #copyray-overlay * のグローバルリセットは不要。
|
|
75
3
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.copyray-specimen-handle {
|
|
81
|
-
float: left;
|
|
82
|
-
margin: 0 2px 2px 0;
|
|
83
|
-
background: #fff;
|
|
84
|
-
padding: 0 3px;
|
|
85
|
-
color: #333;
|
|
86
|
-
font-size: 10px;
|
|
87
|
-
cursor: pointer;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
.copyray-specimen-handle.Specimen {
|
|
91
|
-
background: rgba(255, 50, 50, 0.8);
|
|
92
|
-
color: #fff;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
a.copyray-toggle-button {
|
|
96
|
-
display: block;
|
|
97
|
-
position: fixed;
|
|
98
|
-
left: 0;
|
|
99
|
-
bottom: 0;
|
|
100
|
-
color: white;
|
|
101
|
-
background: black;
|
|
102
|
-
padding: 12px 16px;
|
|
103
|
-
border-radius: 0 10px 0 0;
|
|
104
|
-
opacity: 0;
|
|
105
|
-
transition: opacity 0.6s ease-in-out;
|
|
106
|
-
z-index: 10000;
|
|
107
|
-
font-size: 12px;
|
|
108
|
-
cursor: pointer;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
a.copyray-toggle-button:hover {
|
|
112
|
-
opacity: 1;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
#copy-tuner-bar {
|
|
4
|
+
// ツールバー(<copytuner-bar>)のスタイル。:host にバー本体のレイアウトを定義する。
|
|
5
|
+
export const BAR_STYLES = `
|
|
6
|
+
:host {
|
|
116
7
|
position: fixed;
|
|
117
8
|
left: 0;
|
|
118
9
|
right: 0;
|
|
@@ -126,44 +17,45 @@ a.copyray-toggle-button:hover {
|
|
|
126
17
|
z-index: 2147483647;
|
|
127
18
|
box-shadow: 0 -1px 0 rgba(255, 255, 255, 0.1), inset 0 2px 6px rgba(0, 0, 0, 0.8);
|
|
128
19
|
background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.3));
|
|
20
|
+
box-sizing: border-box;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
:host([hidden]) {
|
|
24
|
+
display: none;
|
|
129
25
|
}
|
|
130
26
|
|
|
131
|
-
|
|
27
|
+
.log-menu {
|
|
132
28
|
position: fixed;
|
|
133
29
|
left: 0;
|
|
134
30
|
right: 0;
|
|
135
31
|
bottom: 40px;
|
|
136
32
|
max-height: calc(100vh - 40px);
|
|
137
33
|
background: #222;
|
|
138
|
-
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
139
34
|
color: #fff;
|
|
140
|
-
z-index: 2147483647;
|
|
141
35
|
overflow-y: auto;
|
|
142
36
|
}
|
|
143
37
|
|
|
144
|
-
|
|
38
|
+
.log-menu[hidden] {
|
|
39
|
+
display: none;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.log-menu tbody td {
|
|
145
43
|
padding: 2px 8px;
|
|
146
44
|
}
|
|
147
45
|
|
|
148
|
-
|
|
46
|
+
.log-menu tbody tr {
|
|
149
47
|
cursor: pointer;
|
|
150
48
|
}
|
|
151
49
|
|
|
152
|
-
|
|
50
|
+
.log-menu tbody tr:hover {
|
|
153
51
|
background: #444;
|
|
154
52
|
}
|
|
155
53
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
#copy-tuner-bar-log-menu tbody a:hover,
|
|
161
|
-
#copy-tuner-bar-log-menu tbody a:focus {
|
|
162
|
-
color: #fff;
|
|
163
|
-
text-decoration: underline;
|
|
54
|
+
.log-menu tbody tr[hidden] {
|
|
55
|
+
display: none;
|
|
164
56
|
}
|
|
165
57
|
|
|
166
|
-
.
|
|
58
|
+
.button {
|
|
167
59
|
position: relative;
|
|
168
60
|
display: inline-block;
|
|
169
61
|
color: #fff;
|
|
@@ -180,16 +72,17 @@ a.copyray-toggle-button:hover {
|
|
|
180
72
|
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
|
181
73
|
inset 0 0 2px rgba(255, 255, 255, 0.2);
|
|
182
74
|
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.4);
|
|
75
|
+
text-decoration: none;
|
|
183
76
|
}
|
|
184
77
|
|
|
185
|
-
.
|
|
186
|
-
.
|
|
78
|
+
.button:hover,
|
|
79
|
+
.button:focus {
|
|
187
80
|
color: #fff;
|
|
188
81
|
text-decoration: none;
|
|
189
82
|
background-color: #555;
|
|
190
83
|
}
|
|
191
84
|
|
|
192
|
-
.
|
|
85
|
+
.notice {
|
|
193
86
|
display: inline-block;
|
|
194
87
|
margin: 8px;
|
|
195
88
|
font-size: 13px;
|
|
@@ -198,9 +91,7 @@ a.copyray-toggle-button:hover {
|
|
|
198
91
|
color: #ffd24d;
|
|
199
92
|
}
|
|
200
93
|
|
|
201
|
-
|
|
202
|
-
-webkit-appearance: none;
|
|
203
|
-
-moz-appearance: none;
|
|
94
|
+
.search {
|
|
204
95
|
appearance: none;
|
|
205
96
|
border: none;
|
|
206
97
|
border-radius: 2px;
|
|
@@ -215,13 +106,87 @@ input[type='text'].copy-tuner-bar__search {
|
|
|
215
106
|
height: auto;
|
|
216
107
|
font-size: 14px;
|
|
217
108
|
}
|
|
109
|
+
`
|
|
110
|
+
|
|
111
|
+
// オーバーレイ(<copyray-overlay>)のスタイル。
|
|
112
|
+
// :host はドキュメント原点基準(position: absolute; top/left: 0)にして、
|
|
113
|
+
// 子の specimen を computeBoundingBox のページ座標で absolute 配置できるようにする。
|
|
114
|
+
// 背景の暗転(.backdrop)だけは viewport 固定(fixed)にする。
|
|
115
|
+
export const OVERLAY_STYLES = `
|
|
116
|
+
:host {
|
|
117
|
+
position: absolute;
|
|
118
|
+
top: 0;
|
|
119
|
+
left: 0;
|
|
120
|
+
width: 0;
|
|
121
|
+
height: 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
:host([hidden]) {
|
|
125
|
+
display: none;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.backdrop {
|
|
129
|
+
position: fixed;
|
|
130
|
+
inset: 0;
|
|
131
|
+
background-image: radial-gradient(
|
|
132
|
+
ellipse farthest-corner at center,
|
|
133
|
+
rgba(0, 0, 0, 0.4) 10%,
|
|
134
|
+
rgba(0, 0, 0, 0.8) 100%
|
|
135
|
+
);
|
|
136
|
+
z-index: 9000;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.specimen {
|
|
140
|
+
position: absolute;
|
|
141
|
+
background: rgba(255, 50, 50, 0.1);
|
|
142
|
+
outline: 1px solid rgba(255, 50, 50, 0.8);
|
|
143
|
+
outline-offset: -1px;
|
|
144
|
+
color: #666;
|
|
145
|
+
font-family: 'Helvetica Neue', sans-serif;
|
|
146
|
+
font-size: 13px;
|
|
147
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
|
|
148
|
+
z-index: 2000000000;
|
|
149
|
+
}
|
|
218
150
|
|
|
219
|
-
.
|
|
220
|
-
|
|
151
|
+
.specimen:hover {
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
background: rgba(255, 50, 50, 0.4);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.specimen-handle {
|
|
157
|
+
float: left;
|
|
158
|
+
margin: 0 2px 2px 0;
|
|
159
|
+
background: rgba(255, 50, 50, 0.8);
|
|
160
|
+
padding: 0 3px;
|
|
161
|
+
color: #fff;
|
|
162
|
+
font-size: 10px;
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
}
|
|
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;
|
|
221
185
|
}
|
|
222
186
|
|
|
223
187
|
@media screen and (max-width: 480px) {
|
|
224
|
-
.
|
|
225
|
-
display: none
|
|
188
|
+
.toggle-button {
|
|
189
|
+
display: none;
|
|
226
190
|
}
|
|
227
191
|
}
|
|
192
|
+
`
|
data/src/util.ts
CHANGED
|
@@ -35,4 +35,12 @@ const computeBoundingBox = (element) => {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
const debounce = <A extends unknown[]>(fn: (...args: A) => void, wait: number) => {
|
|
39
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
40
|
+
return (...args: A) => {
|
|
41
|
+
clearTimeout(timer)
|
|
42
|
+
timer = setTimeout(() => fn(...args), wait)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { computeBoundingBox, debounce, getOffset, isMac, isVisible }
|
data/tsconfig.json
CHANGED
|
@@ -3,11 +3,9 @@
|
|
|
3
3
|
"target": "ESNext",
|
|
4
4
|
"useDefineForClassFields": true,
|
|
5
5
|
"module": "ESNext",
|
|
6
|
-
"lib": [
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
],
|
|
10
|
-
"moduleResolution": "Node",
|
|
6
|
+
"lib": ["ESNext", "DOM"],
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"verbatimModuleSyntax": true,
|
|
11
9
|
"strict": true,
|
|
12
10
|
"sourceMap": true,
|
|
13
11
|
"resolveJsonModule": true,
|
|
@@ -17,10 +15,7 @@
|
|
|
17
15
|
"noUnusedLocals": true,
|
|
18
16
|
"noUnusedParameters": true,
|
|
19
17
|
"noImplicitReturns": true,
|
|
20
|
-
"skipLibCheck": true
|
|
18
|
+
"skipLibCheck": true
|
|
21
19
|
},
|
|
22
|
-
"include": [
|
|
23
|
-
"client",
|
|
24
|
-
"src",
|
|
25
|
-
]
|
|
20
|
+
"include": ["src"]
|
|
26
21
|
}
|