copy_tuner_client 0.12.0 → 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.eslintrc.js +12 -0
- data/.gitattributes +2 -0
- data/.github/workflows/rspec.yml +4 -8
- data/.ruby-version +1 -1
- data/.vscode/settings.json +7 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile.lock +117 -116
- data/README.md +5 -10
- data/app/assets/javascripts/main.js +388 -0
- data/app/assets/stylesheets/style.css +1 -0
- data/copy_tuner_client.gemspec +1 -1
- data/gemfiles/{5.2.gemfile → 7.0.gemfile} +1 -1
- data/index.html +34 -0
- data/lib/copy_tuner_client/copyray_middleware.rb +15 -29
- data/lib/copy_tuner_client/engine.rb +2 -2
- data/lib/copy_tuner_client/version.rb +1 -1
- data/package.json +14 -15
- data/spec/copy_tuner_client/i18n_backend_spec.rb +2 -0
- data/{app/assets/stylesheets → src}/copyray.css +0 -0
- data/src/copyray.ts +130 -0
- data/src/copytuner_bar.ts +115 -0
- data/src/main.ts +58 -0
- data/src/specimen.ts +84 -0
- data/src/util.ts +38 -0
- data/src/vite-env.d.ts +1 -0
- data/tsconfig.json +26 -0
- data/vite.config.ts +18 -0
- data/yarn.lock +2251 -0
- metadata +25 -22
- data/.eslintrc +0 -4
- data/app/assets/javascripts/copyray.js +0 -1069
- data/app/views/_copy_tuner_bar.html.erb +0 -6
- data/gemfiles/6.0.gemfile +0 -5
- data/package-lock.json +0 -4341
- data/rollup.config.js +0 -16
- data/src/copyray.js +0 -112
- data/src/copytuner_bar.js +0 -96
- data/src/main.js +0 -42
- data/src/specimen.js +0 -63
- data/src/util.js +0 -32
- data/test.sh +0 -6
data/src/copyray.ts
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
import CopyTunerBar from './copytuner_bar'
|
2
|
+
import Specimen from './specimen'
|
3
|
+
|
4
|
+
const findBlurbs = () => {
|
5
|
+
const filterNone = () => NodeFilter.FILTER_ACCEPT
|
6
|
+
|
7
|
+
// @ts-expect-error TS2554
|
8
|
+
const iterator = document.createNodeIterator(document.body, NodeFilter.SHOW_COMMENT, filterNone, false)
|
9
|
+
|
10
|
+
const comments = []
|
11
|
+
let curNode
|
12
|
+
|
13
|
+
while ((curNode = iterator.nextNode())) {
|
14
|
+
comments.push(curNode)
|
15
|
+
}
|
16
|
+
|
17
|
+
return (
|
18
|
+
comments
|
19
|
+
// @ts-expect-error TS2531
|
20
|
+
.filter((comment) => comment.nodeValue.startsWith('COPYRAY'))
|
21
|
+
.map((comment) => {
|
22
|
+
// @ts-expect-error TS2488
|
23
|
+
const [, key] = comment.nodeValue.match(/^COPYRAY (\S*)$/)
|
24
|
+
const element = comment.parentNode
|
25
|
+
return { key, element }
|
26
|
+
})
|
27
|
+
)
|
28
|
+
}
|
29
|
+
|
30
|
+
export default class Copyray {
|
31
|
+
// @ts-expect-error TS7006
|
32
|
+
constructor(baseUrl, data) {
|
33
|
+
// @ts-expect-error TS2339
|
34
|
+
this.baseUrl = baseUrl
|
35
|
+
// @ts-expect-error TS2339
|
36
|
+
this.data = data
|
37
|
+
// @ts-expect-error TS2339
|
38
|
+
this.isShowing = false
|
39
|
+
// @ts-expect-error TS2339
|
40
|
+
this.specimens = []
|
41
|
+
// @ts-expect-error TS2339
|
42
|
+
this.overlay = this.makeOverlay()
|
43
|
+
// @ts-expect-error TS2339
|
44
|
+
this.toggleButton = this.makeToggleButton()
|
45
|
+
// @ts-expect-error TS2339
|
46
|
+
this.boundOpen = this.open.bind(this)
|
47
|
+
|
48
|
+
// @ts-expect-error TS2339
|
49
|
+
this.copyTunerBar = new CopyTunerBar(document.querySelector('#copy-tuner-bar'), this.data, this.boundOpen)
|
50
|
+
}
|
51
|
+
|
52
|
+
show() {
|
53
|
+
this.reset()
|
54
|
+
|
55
|
+
// @ts-expect-error TS2339
|
56
|
+
document.body.append(this.overlay)
|
57
|
+
this.makeSpecimens()
|
58
|
+
|
59
|
+
// @ts-expect-error TS2339
|
60
|
+
for (const specimen of this.specimens) {
|
61
|
+
specimen.show()
|
62
|
+
}
|
63
|
+
|
64
|
+
// @ts-expect-error TS2339
|
65
|
+
this.copyTunerBar.show()
|
66
|
+
// @ts-expect-error TS2339
|
67
|
+
this.isShowing = true
|
68
|
+
}
|
69
|
+
|
70
|
+
hide() {
|
71
|
+
// @ts-expect-error TS2339
|
72
|
+
this.overlay.remove()
|
73
|
+
this.reset()
|
74
|
+
// @ts-expect-error TS2339
|
75
|
+
this.copyTunerBar.hide()
|
76
|
+
// @ts-expect-error TS2339
|
77
|
+
this.isShowing = false
|
78
|
+
}
|
79
|
+
|
80
|
+
toggle() {
|
81
|
+
// @ts-expect-error TS2339
|
82
|
+
if (this.isShowing) {
|
83
|
+
this.hide()
|
84
|
+
} else {
|
85
|
+
this.show()
|
86
|
+
}
|
87
|
+
}
|
88
|
+
|
89
|
+
// @ts-expect-error TS7006
|
90
|
+
open(key) {
|
91
|
+
// @ts-expect-error TS2339
|
92
|
+
window.open(`${this.baseUrl}/blurbs/${key}/edit`)
|
93
|
+
}
|
94
|
+
|
95
|
+
makeSpecimens() {
|
96
|
+
for (const { element, key } of findBlurbs()) {
|
97
|
+
// @ts-expect-error TS2339
|
98
|
+
this.specimens.push(new Specimen(element, key, this.boundOpen))
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
102
|
+
makeToggleButton() {
|
103
|
+
const element = document.createElement('a')
|
104
|
+
|
105
|
+
element.addEventListener('click', () => {
|
106
|
+
this.show()
|
107
|
+
})
|
108
|
+
|
109
|
+
element.classList.add('copyray-toggle-button')
|
110
|
+
element.classList.add('hidden-on-mobile')
|
111
|
+
element.textContent = 'Open CopyTuner'
|
112
|
+
document.body.append(element)
|
113
|
+
|
114
|
+
return element
|
115
|
+
}
|
116
|
+
|
117
|
+
makeOverlay() {
|
118
|
+
const div = document.createElement('div')
|
119
|
+
div.setAttribute('id', 'copyray-overlay')
|
120
|
+
div.addEventListener('click', () => this.hide())
|
121
|
+
return div
|
122
|
+
}
|
123
|
+
|
124
|
+
reset() {
|
125
|
+
// @ts-expect-error TS2339
|
126
|
+
for (const specimen of this.specimens) {
|
127
|
+
specimen.remove()
|
128
|
+
}
|
129
|
+
}
|
130
|
+
}
|
@@ -0,0 +1,115 @@
|
|
1
|
+
// @ts-expect-error TS7016
|
2
|
+
import debounce from 'lodash.debounce'
|
3
|
+
|
4
|
+
const HIDDEN_CLASS = 'copy-tuner-hidden'
|
5
|
+
|
6
|
+
export default class CopytunerBar {
|
7
|
+
// @ts-expect-error TS7006
|
8
|
+
constructor(element, data, callback) {
|
9
|
+
// @ts-expect-error TS2339
|
10
|
+
this.element = element
|
11
|
+
// @ts-expect-error TS2339
|
12
|
+
this.data = data
|
13
|
+
// @ts-expect-error TS2339
|
14
|
+
this.callback = callback
|
15
|
+
// @ts-expect-error TS2339
|
16
|
+
this.searchBoxElement = element.querySelector('.js-copy-tuner-bar-search')
|
17
|
+
// @ts-expect-error TS2339
|
18
|
+
this.logMenuElement = this.makeLogMenu()
|
19
|
+
// @ts-expect-error TS2339
|
20
|
+
this.element.append(this.logMenuElement)
|
21
|
+
|
22
|
+
this.addHandler()
|
23
|
+
}
|
24
|
+
|
25
|
+
addHandler() {
|
26
|
+
// @ts-expect-error TS2339
|
27
|
+
const openLogButton = this.element.querySelector('.js-copy-tuner-bar-open-log')
|
28
|
+
// @ts-expect-error TS7006
|
29
|
+
openLogButton.addEventListener('click', (event) => {
|
30
|
+
event.preventDefault()
|
31
|
+
this.toggleLogMenu()
|
32
|
+
})
|
33
|
+
|
34
|
+
// @ts-expect-error TS2339
|
35
|
+
this.searchBoxElement.addEventListener('input', debounce(this.onKeyup.bind(this), 250))
|
36
|
+
}
|
37
|
+
|
38
|
+
show() {
|
39
|
+
// @ts-expect-error TS2339
|
40
|
+
this.element.classList.remove(HIDDEN_CLASS)
|
41
|
+
// @ts-expect-error TS2339
|
42
|
+
this.searchBoxElement.focus()
|
43
|
+
}
|
44
|
+
|
45
|
+
hide() {
|
46
|
+
// @ts-expect-error TS2339
|
47
|
+
this.element.classList.add(HIDDEN_CLASS)
|
48
|
+
}
|
49
|
+
|
50
|
+
showLogMenu() {
|
51
|
+
// @ts-expect-error TS2339
|
52
|
+
this.logMenuElement.classList.remove(HIDDEN_CLASS)
|
53
|
+
}
|
54
|
+
|
55
|
+
toggleLogMenu() {
|
56
|
+
// @ts-expect-error TS2339
|
57
|
+
this.logMenuElement.classList.toggle(HIDDEN_CLASS)
|
58
|
+
}
|
59
|
+
|
60
|
+
makeLogMenu() {
|
61
|
+
const div = document.createElement('div')
|
62
|
+
div.setAttribute('id', 'copy-tuner-bar-log-menu')
|
63
|
+
div.classList.add(HIDDEN_CLASS)
|
64
|
+
|
65
|
+
const table = document.createElement('table')
|
66
|
+
const tbody = document.createElement('tbody')
|
67
|
+
tbody.classList.remove('is-not-initialized')
|
68
|
+
|
69
|
+
// @ts-expect-error TS2339
|
70
|
+
for (const key of Object.keys(this.data).sort()) {
|
71
|
+
// @ts-expect-error TS2339
|
72
|
+
const value = this.data[key]
|
73
|
+
|
74
|
+
if (value === '') {
|
75
|
+
continue
|
76
|
+
}
|
77
|
+
|
78
|
+
const td1 = document.createElement('td')
|
79
|
+
td1.textContent = key
|
80
|
+
const td2 = document.createElement('td')
|
81
|
+
td2.textContent = value
|
82
|
+
const tr = document.createElement('tr')
|
83
|
+
tr.classList.add('copy-tuner-bar-log-menu__row')
|
84
|
+
tr.dataset.key = key
|
85
|
+
|
86
|
+
tr.addEventListener('click', ({ currentTarget }) => {
|
87
|
+
// @ts-expect-error TS2339
|
88
|
+
this.callback(currentTarget.dataset.key)
|
89
|
+
})
|
90
|
+
|
91
|
+
tr.append(td1)
|
92
|
+
tr.append(td2)
|
93
|
+
tbody.append(tr)
|
94
|
+
}
|
95
|
+
|
96
|
+
table.append(tbody)
|
97
|
+
div.append(table)
|
98
|
+
|
99
|
+
return div
|
100
|
+
}
|
101
|
+
|
102
|
+
// @ts-expect-error TS7031
|
103
|
+
onKeyup({ target }) {
|
104
|
+
const keyword = target.value.trim()
|
105
|
+
this.showLogMenu()
|
106
|
+
|
107
|
+
// @ts-expect-error TS2339
|
108
|
+
const rows = [...this.logMenuElement.querySelectorAll('tr')]
|
109
|
+
|
110
|
+
for (const row of rows) {
|
111
|
+
const isShow = keyword === '' || [...row.querySelectorAll('td')].some((td) => td.textContent.includes(keyword))
|
112
|
+
row.classList.toggle(HIDDEN_CLASS, !isShow)
|
113
|
+
}
|
114
|
+
}
|
115
|
+
}
|
data/src/main.ts
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
/* eslint-disable no-console */
|
2
|
+
import Copyray from './copyray'
|
3
|
+
import { isMac } from './util'
|
4
|
+
|
5
|
+
declare global {
|
6
|
+
interface Window {
|
7
|
+
CopyTuner: {
|
8
|
+
url: string
|
9
|
+
// TODO: type
|
10
|
+
data: object
|
11
|
+
}
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
15
|
+
import './copyray.css'
|
16
|
+
|
17
|
+
// NOTE: 元々railsから出力されいてたマークアップに合わせてひとまず、、
|
18
|
+
const appendCopyTunerBar = (url: string) => {
|
19
|
+
const bar = document.createElement('div')
|
20
|
+
bar.id = 'copy-tuner-bar'
|
21
|
+
bar.classList.add('copy-tuner-hidden')
|
22
|
+
bar.innerHTML = `
|
23
|
+
<a class="copy-tuner-bar-button" target="_blank" href="${url}">CopyTuner</a>
|
24
|
+
<a href="/copytuner" target="_blank" class="copy-tuner-bar-button">Sync</a>
|
25
|
+
<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>
|
26
|
+
<input type="text" class="copy-tuner-bar__search js-copy-tuner-bar-search" placeholder="search">
|
27
|
+
`
|
28
|
+
document.body.append(bar)
|
29
|
+
}
|
30
|
+
|
31
|
+
const start = () => {
|
32
|
+
const { url, data } = window.CopyTuner
|
33
|
+
|
34
|
+
appendCopyTunerBar(url)
|
35
|
+
const copyray = new Copyray(url, data)
|
36
|
+
|
37
|
+
document.addEventListener('keydown', (event) => {
|
38
|
+
// @ts-expect-error TS2339
|
39
|
+
if (copyray.isShowing && ['Escape', 'Esc'].includes(event.key)) {
|
40
|
+
copyray.hide()
|
41
|
+
return
|
42
|
+
}
|
43
|
+
|
44
|
+
if (((isMac && event.metaKey) || (!isMac && event.ctrlKey)) && event.shiftKey && event.key === 'k') {
|
45
|
+
copyray.toggle()
|
46
|
+
}
|
47
|
+
})
|
48
|
+
|
49
|
+
if (console) {
|
50
|
+
console.log(`Ready to Copyray. Press ${isMac ? 'cmd+shift+k' : 'ctrl+shift+k'} to scan your UI.`)
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
if (document.readyState === 'complete' || document.readyState !== 'loading') {
|
55
|
+
start()
|
56
|
+
} else {
|
57
|
+
document.addEventListener('DOMContentLoaded', () => start())
|
58
|
+
}
|
data/src/specimen.ts
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
import { computeBoundingBox } from './util'
|
2
|
+
|
3
|
+
const ZINDEX = 2_000_000_000
|
4
|
+
|
5
|
+
export default class Specimen {
|
6
|
+
// @ts-expect-error TS7006
|
7
|
+
constructor(element, key, callback) {
|
8
|
+
// @ts-expect-error TS2339
|
9
|
+
this.element = element
|
10
|
+
// @ts-expect-error TS2339
|
11
|
+
this.key = key
|
12
|
+
// @ts-expect-error TS2339
|
13
|
+
this.callback = callback
|
14
|
+
}
|
15
|
+
|
16
|
+
show() {
|
17
|
+
// @ts-expect-error TS2339
|
18
|
+
this.box = this.makeBox()
|
19
|
+
// @ts-expect-error TS2339
|
20
|
+
if (this.box === null) return
|
21
|
+
|
22
|
+
// @ts-expect-error TS2339
|
23
|
+
this.box.addEventListener('click', () => {
|
24
|
+
// @ts-expect-error TS2339
|
25
|
+
this.callback(this.key)
|
26
|
+
})
|
27
|
+
|
28
|
+
// @ts-expect-error TS2339
|
29
|
+
document.body.append(this.box)
|
30
|
+
}
|
31
|
+
|
32
|
+
remove() {
|
33
|
+
// @ts-expect-error TS2339
|
34
|
+
if (!this.box) {
|
35
|
+
return
|
36
|
+
}
|
37
|
+
// @ts-expect-error TS2339
|
38
|
+
this.box.remove()
|
39
|
+
// @ts-expect-error TS2339
|
40
|
+
this.box = null
|
41
|
+
}
|
42
|
+
|
43
|
+
makeBox() {
|
44
|
+
const box = document.createElement('div')
|
45
|
+
box.classList.add('copyray-specimen')
|
46
|
+
box.classList.add('Specimen')
|
47
|
+
|
48
|
+
// @ts-expect-error TS2339
|
49
|
+
const bounds = computeBoundingBox(this.element)
|
50
|
+
if (bounds === null) return null
|
51
|
+
|
52
|
+
for (const key of Object.keys(bounds)) {
|
53
|
+
// @ts-expect-error TS7053
|
54
|
+
const value = bounds[key]
|
55
|
+
// @ts-expect-error TS7015
|
56
|
+
box.style[key] = `${value}px`
|
57
|
+
}
|
58
|
+
// @ts-expect-error TS2322
|
59
|
+
box.style.zIndex = ZINDEX
|
60
|
+
|
61
|
+
// @ts-expect-error TS2339
|
62
|
+
const { position, top, left } = getComputedStyle(this.element)
|
63
|
+
if (position === 'fixed') {
|
64
|
+
// @ts-expect-error TS2339
|
65
|
+
this.box.style.position = 'fixed'
|
66
|
+
// @ts-expect-error TS2339
|
67
|
+
this.box.style.top = `${top}px`
|
68
|
+
// @ts-expect-error TS2339
|
69
|
+
this.box.style.left = `${left}px`
|
70
|
+
}
|
71
|
+
|
72
|
+
box.append(this.makeLabel())
|
73
|
+
return box
|
74
|
+
}
|
75
|
+
|
76
|
+
makeLabel() {
|
77
|
+
const div = document.createElement('div')
|
78
|
+
div.classList.add('copyray-specimen-handle')
|
79
|
+
div.classList.add('Specimen')
|
80
|
+
// @ts-expect-error TS2339
|
81
|
+
div.textContent = this.key
|
82
|
+
return div
|
83
|
+
}
|
84
|
+
}
|
data/src/util.ts
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
const isMac = navigator.platform.toUpperCase().includes('MAC')
|
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
|
+
export { isMac, isVisible, getOffset, computeBoundingBox }
|
data/src/vite-env.d.ts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
/// <reference types="vite/client" />
|
data/tsconfig.json
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"target": "ESNext",
|
4
|
+
"useDefineForClassFields": true,
|
5
|
+
"module": "ESNext",
|
6
|
+
"lib": [
|
7
|
+
"ESNext",
|
8
|
+
"DOM"
|
9
|
+
],
|
10
|
+
"moduleResolution": "Node",
|
11
|
+
"strict": true,
|
12
|
+
"sourceMap": true,
|
13
|
+
"resolveJsonModule": true,
|
14
|
+
"isolatedModules": true,
|
15
|
+
"esModuleInterop": true,
|
16
|
+
"noEmit": true,
|
17
|
+
"noUnusedLocals": true,
|
18
|
+
"noUnusedParameters": true,
|
19
|
+
"noImplicitReturns": true,
|
20
|
+
"skipLibCheck": true,
|
21
|
+
},
|
22
|
+
"include": [
|
23
|
+
"client",
|
24
|
+
"src",
|
25
|
+
]
|
26
|
+
}
|
data/vite.config.ts
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
import { defineConfig } from 'vite'
|
2
|
+
|
3
|
+
// https://vitejs.dev/config/
|
4
|
+
export default defineConfig({
|
5
|
+
build: {
|
6
|
+
outDir: './app/assets',
|
7
|
+
lib: {
|
8
|
+
entry: 'src/main.ts',
|
9
|
+
formats: ['es'],
|
10
|
+
},
|
11
|
+
rollupOptions: {
|
12
|
+
output: {
|
13
|
+
entryFileNames: `javascripts/[name].js`,
|
14
|
+
assetFileNames: `stylesheets/[name].[ext]`,
|
15
|
+
}
|
16
|
+
}
|
17
|
+
},
|
18
|
+
})
|