@cu-mkp/editioncrafter 1.3.0-beta.1 → 1.3.0-beta.3
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 +7 -0
- package/dist/editioncrafter.js +34747 -31554
- package/dist/es/src/EditionCrafter/component/DiploMatic.js +19 -17
- package/dist/es/src/EditionCrafter/component/DocumentView.js +13 -9
- package/dist/es/src/EditionCrafter/component/EmptyPaneView.js +22 -0
- package/dist/es/src/EditionCrafter/component/ImageView.js +61 -40
- package/dist/es/src/EditionCrafter/component/Navigation.js +1 -0
- package/dist/es/src/EditionCrafter/component/SplitPaneView.js +10 -9
- package/dist/es/src/EditionCrafter/component/TagToolbar.jsx +14 -4
- package/dist/es/src/EditionCrafter/context/TagFilter.jsx +22 -11
- package/dist/es/src/EditionCrafter/context/TagFilterContext.js +2 -1
- package/dist/es/src/EditionCrafter/index.jsx +31 -0
- package/dist/es/src/EditionCrafter/model/Folio.js +27 -1
- package/dist/es/src/EditionCrafter/saga/RouteListenerSaga.js +22 -7
- package/dist/es/src/EditionCrafter/scss/_imageView.scss +1 -1
- package/dist/es/src/RecordList/component/PhraseListTable.jsx +108 -0
- package/dist/es/src/RecordList/component/Record.jsx +38 -2
- package/dist/es/src/RecordList/component/RecordListView.jsx +1 -1
- package/dist/es/src/RecordList/component/Sidebar.jsx +1 -1
- package/dist/es/src/RecordList/component/SidebarTagList.jsx +1 -1
- package/dist/es/src/RecordList/index.jsx +10 -5
- package/dist/es/src/RecordList/styles/base.css +2 -17
- package/dist/es/src/RecordList/styles/record.css +92 -4
- package/dist/es/src/TagExplore/assets/InsertLeft.jsx +18 -0
- package/dist/es/src/TagExplore/assets/InsertRight.jsx +18 -0
- package/dist/es/src/TagExplore/assets/Left.jsx +17 -0
- package/dist/es/src/TagExplore/assets/Right.jsx +17 -0
- package/dist/es/src/TagExplore/assets/insert_left.svg +12 -0
- package/dist/es/src/TagExplore/assets/insert_right.svg +12 -0
- package/dist/es/src/TagExplore/assets/left.svg +11 -0
- package/dist/es/src/TagExplore/assets/right.svg +11 -0
- package/dist/es/src/TagExplore/components/DocumentDetail.jsx +224 -0
- package/dist/es/src/TagExplore/components/NarrowSidebar.jsx +35 -0
- package/dist/es/src/TagExplore/components/SurfaceBrowser.jsx +176 -0
- package/dist/es/src/TagExplore/components/TagExploreSidebar.jsx +55 -0
- package/dist/es/src/TagExplore/components/TagFilters.jsx +106 -0
- package/dist/es/src/TagExplore/index.jsx +109 -0
- package/dist/es/src/TagExplore/styles/base.css +192 -0
- package/dist/es/src/{RecordList/component → common/components}/Pill.jsx +2 -0
- package/dist/es/src/common/components/Pill.style.css +16 -0
- package/dist/es/src/index.jsx +3 -27
- package/package.json +2 -28
- /package/dist/es/src/{RecordList/component → common/components}/Loading.jsx +0 -0
- /package/dist/es/src/{RecordList → common}/lib/sql.js +0 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { Accordion, AccordionDetails, AccordionSummary, Grid, Typography } from '@material-ui/core'
|
|
2
|
+
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
|
|
3
|
+
import { useEffect, useMemo } from 'react'
|
|
4
|
+
import { getObjs } from '../../common/lib/sql'
|
|
5
|
+
import InsertLeft from '../assets/InsertLeft'
|
|
6
|
+
import InsertRight from '../assets/InsertRight'
|
|
7
|
+
import Left from '../assets/Left'
|
|
8
|
+
import Right from '../assets/Right'
|
|
9
|
+
|
|
10
|
+
const MAX_THUMBNAIL_DIMENSION = 100
|
|
11
|
+
|
|
12
|
+
function getData(db, docID, tags = []) {
|
|
13
|
+
const surfaceStmt = db.prepare(`
|
|
14
|
+
SELECT
|
|
15
|
+
surfaces.id AS id,
|
|
16
|
+
surfaces.name AS name,
|
|
17
|
+
surfaces.xml_id AS xml_id,
|
|
18
|
+
surfaces.width AS width,
|
|
19
|
+
surfaces.height AS height,
|
|
20
|
+
surfaces.image_type AS image_type,
|
|
21
|
+
surfaces.image_url AS image_url,
|
|
22
|
+
surfaces.position AS position,
|
|
23
|
+
taggings.tag_id
|
|
24
|
+
FROM
|
|
25
|
+
surfaces
|
|
26
|
+
LEFT JOIN
|
|
27
|
+
elements
|
|
28
|
+
ON
|
|
29
|
+
surfaces.id = elements.surface_id
|
|
30
|
+
LEFT JOIN
|
|
31
|
+
taggings
|
|
32
|
+
ON
|
|
33
|
+
elements.id = taggings.element_id
|
|
34
|
+
WHERE
|
|
35
|
+
document_id=${docID}${tags.length ? ` AND taggings.tag_id IN (${tags.join(',')})` : ''}
|
|
36
|
+
ORDER BY
|
|
37
|
+
surfaces.id, position
|
|
38
|
+
`)
|
|
39
|
+
|
|
40
|
+
// add thumbnail URLs
|
|
41
|
+
const surfaces = getObjs(surfaceStmt)
|
|
42
|
+
for (const surface of surfaces) {
|
|
43
|
+
surface.thumbnail_url = getThumbnailURL(surface)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return surfaces
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getThumbnailURL(surface) {
|
|
50
|
+
const { image_url, width, height, image_type } = surface
|
|
51
|
+
|
|
52
|
+
const ratio = height !== 0 ? width / height : 0
|
|
53
|
+
|
|
54
|
+
let thumbnailDimensions = []
|
|
55
|
+
if (ratio > 1) {
|
|
56
|
+
thumbnailDimensions = [MAX_THUMBNAIL_DIMENSION, Math.round(MAX_THUMBNAIL_DIMENSION / ratio)]
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
thumbnailDimensions = [Math.round(MAX_THUMBNAIL_DIMENSION * ratio), MAX_THUMBNAIL_DIMENSION]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const thumbnailURL = image_type === 'iiif'
|
|
63
|
+
? `${image_url}/full/${thumbnailDimensions.join(',')}/0/default.jpg`
|
|
64
|
+
: image_url
|
|
65
|
+
|
|
66
|
+
return thumbnailURL
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function Thumbnail(props) {
|
|
70
|
+
const { surfaceID, name, imageURL, thumbnailURL, onClick, selection, info } = props
|
|
71
|
+
|
|
72
|
+
const isLeft = useMemo(() => (selection && selection?.left?.localID === info?.localID && selection?.left?.surfaceID === info?.surfaceID), [selection, info])
|
|
73
|
+
const isRight = useMemo(() => (selection && selection?.right?.localID === info?.localID && selection?.right?.surfaceID === info?.surfaceID), [selection, info])
|
|
74
|
+
|
|
75
|
+
const onError = (currentTarget) => {
|
|
76
|
+
currentTarget.onerror = null
|
|
77
|
+
const fullImageURL = `${imageURL.slice(0, -9)}full/full/0/default.jpg`
|
|
78
|
+
if (currentTarget.src !== fullImageURL) {
|
|
79
|
+
currentTarget.src = fullImageURL
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<Grid className="surface-thumbnail">
|
|
85
|
+
<figure className="surface-thumbnail-figure">
|
|
86
|
+
{isLeft
|
|
87
|
+
? (
|
|
88
|
+
<div className="surface-thumbnail-overlay-selected left">
|
|
89
|
+
<div>
|
|
90
|
+
<Left />
|
|
91
|
+
<Typography>Left</Typography>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
: isRight
|
|
96
|
+
? (
|
|
97
|
+
<div className="surface-thumbnail-overlay-selected right">
|
|
98
|
+
<div>
|
|
99
|
+
<Right />
|
|
100
|
+
<Typography>Right</Typography>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
: (
|
|
105
|
+
<div className="surface-thumbnail-overlay">
|
|
106
|
+
<div className="surface-thumbnail-overlay-content">
|
|
107
|
+
<a onClick={onClick.left} title="Insert left"><InsertLeft /></a>
|
|
108
|
+
<a onClick={onClick.right} title="Insert right"><InsertRight /></a>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
<div id={surfaceID}>
|
|
113
|
+
<img
|
|
114
|
+
src={thumbnailURL}
|
|
115
|
+
alt={name}
|
|
116
|
+
style={{ maxWidth: `${MAX_THUMBNAIL_DIMENSION}px`, maxHeight: `${MAX_THUMBNAIL_DIMENSION}px` }}
|
|
117
|
+
onError={onError}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
</figure>
|
|
121
|
+
<figcaption className="surface-thumbnail-caption">
|
|
122
|
+
{name}
|
|
123
|
+
</figcaption>
|
|
124
|
+
</Grid>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function ThumbnailGrid(props) {
|
|
129
|
+
const { surfaces, selection, navigateToSelection, documentLocalID } = props
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<Grid className="thumbnail-grid" container>
|
|
133
|
+
{
|
|
134
|
+
surfaces.map(surface => (
|
|
135
|
+
<Thumbnail
|
|
136
|
+
key={`doc-detail-thumb-${surface.id}`}
|
|
137
|
+
surfaceID={surface.id}
|
|
138
|
+
name={surface.name}
|
|
139
|
+
imageURL={surface.image_url}
|
|
140
|
+
thumbnailURL={surface.thumbnail_url}
|
|
141
|
+
selection={selection}
|
|
142
|
+
info={{
|
|
143
|
+
localID: documentLocalID,
|
|
144
|
+
surfaceID: surface.xml_id,
|
|
145
|
+
}}
|
|
146
|
+
onClick={{
|
|
147
|
+
left: (e) => {
|
|
148
|
+
e.preventDefault()
|
|
149
|
+
navigateToSelection({
|
|
150
|
+
left: {
|
|
151
|
+
localID: documentLocalID,
|
|
152
|
+
surfaceID: surface.xml_id,
|
|
153
|
+
},
|
|
154
|
+
right: selection.right,
|
|
155
|
+
})
|
|
156
|
+
},
|
|
157
|
+
right: (e) => {
|
|
158
|
+
e.preventDefault()
|
|
159
|
+
navigateToSelection({
|
|
160
|
+
left: selection.left,
|
|
161
|
+
right: {
|
|
162
|
+
localID: documentLocalID,
|
|
163
|
+
surfaceID: surface.xml_id,
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
},
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
</Thumbnail>
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
</Grid>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function DocumentDetail(props) {
|
|
178
|
+
const { db, documentName, documentID, documentLocalID, navigateToSelection, updatePageCount, selection, tags } = props
|
|
179
|
+
const surfaces = useMemo(() => {
|
|
180
|
+
const taggedSurfaces = getData(db, documentID, tags)
|
|
181
|
+
const data = []
|
|
182
|
+
for (const entry of taggedSurfaces) {
|
|
183
|
+
if (!data.find(d => (d.id === entry.id))) {
|
|
184
|
+
const tags = taggedSurfaces.filter(s => (s.id === entry.id)).map(s => (s.tag_id))
|
|
185
|
+
data.push({
|
|
186
|
+
...entry,
|
|
187
|
+
tag_id: [...new Set(tags)],
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return data.filter(s => (tags.every(t => s.tag_id.includes(t))))
|
|
192
|
+
}, [db, documentID, tags])
|
|
193
|
+
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
updatePageCount(surfaces?.length)
|
|
196
|
+
}, [surfaces, updatePageCount, tags])
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<Accordion>
|
|
200
|
+
<AccordionSummary
|
|
201
|
+
expandIcon={<ExpandMoreIcon />}
|
|
202
|
+
aria-controls={`document-detail-${documentID}-content`}
|
|
203
|
+
id={`document-detail-${documentID}`}
|
|
204
|
+
className="accordion-summary"
|
|
205
|
+
>
|
|
206
|
+
<Typography>{documentName}</Typography>
|
|
207
|
+
<Typography>{surfaces?.length || ''}</Typography>
|
|
208
|
+
</AccordionSummary>
|
|
209
|
+
<AccordionDetails
|
|
210
|
+
className="accordion-detail"
|
|
211
|
+
>
|
|
212
|
+
<ThumbnailGrid
|
|
213
|
+
navigateToSelection={navigateToSelection}
|
|
214
|
+
documentLocalID={documentLocalID}
|
|
215
|
+
surfaces={surfaces}
|
|
216
|
+
selection={selection}
|
|
217
|
+
>
|
|
218
|
+
</ThumbnailGrid>
|
|
219
|
+
</AccordionDetails>
|
|
220
|
+
</Accordion>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export default DocumentDetail
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FaQuestionCircle
|
|
3
|
+
} from 'react-icons/fa'
|
|
4
|
+
|
|
5
|
+
import { BsFillGrid3X3GapFill } from 'react-icons/bs'
|
|
6
|
+
import { List, ListItem, ListItemIcon, Paper } from '@material-ui/core'
|
|
7
|
+
|
|
8
|
+
function NarrowSidebar(props) {
|
|
9
|
+
const { helpRef, toggleDrawer, toggleHelp } = props
|
|
10
|
+
return (
|
|
11
|
+
<Paper className="narrow-sidebar">
|
|
12
|
+
<List>
|
|
13
|
+
<ListItem
|
|
14
|
+
onClick={toggleDrawer}
|
|
15
|
+
button
|
|
16
|
+
>
|
|
17
|
+
<ListItemIcon>
|
|
18
|
+
<BsFillGrid3X3GapFill />
|
|
19
|
+
</ListItemIcon>
|
|
20
|
+
</ListItem>
|
|
21
|
+
<ListItem
|
|
22
|
+
onClick={toggleHelp}
|
|
23
|
+
ref={helpRef}
|
|
24
|
+
button
|
|
25
|
+
>
|
|
26
|
+
<ListItemIcon>
|
|
27
|
+
<FaQuestionCircle />
|
|
28
|
+
</ListItemIcon>
|
|
29
|
+
</ListItem>
|
|
30
|
+
</List>
|
|
31
|
+
</Paper>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default NarrowSidebar
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { Box, Button, ButtonGroup, Collapse, Divider, IconButton, Typography } from '@material-ui/core'
|
|
2
|
+
import { red } from '@material-ui/core/colors'
|
|
3
|
+
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'
|
|
4
|
+
import GridOnIcon from '@material-ui/icons/GridOn'
|
|
5
|
+
import ListIcon from '@material-ui/icons/List'
|
|
6
|
+
import TuneIcon from '@material-ui/icons/Tune'
|
|
7
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
8
|
+
|
|
9
|
+
import { useLocation, useNavigate } from 'react-router-dom'
|
|
10
|
+
import { getObjs } from '../../common/lib/sql'
|
|
11
|
+
import DocumentDetail from './DocumentDetail'
|
|
12
|
+
import TagFilters from './TagFilters'
|
|
13
|
+
|
|
14
|
+
function getData(db) {
|
|
15
|
+
const docStmt = db.prepare(`
|
|
16
|
+
SELECT
|
|
17
|
+
documents.id AS id,
|
|
18
|
+
documents.name AS name,
|
|
19
|
+
documents.local_id AS local_id
|
|
20
|
+
FROM
|
|
21
|
+
documents
|
|
22
|
+
`)
|
|
23
|
+
|
|
24
|
+
return getObjs(docStmt)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseFolioID(folioID) {
|
|
28
|
+
if (!folioID) {
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
const parts = folioID.split('_')
|
|
32
|
+
const localID = parts.slice(0, parts.length - 1).join('_')
|
|
33
|
+
const surfaceID = parts[parts.length - 1]
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
localID,
|
|
37
|
+
surfaceID,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getSelection(path) {
|
|
42
|
+
const parts = path.split('/')
|
|
43
|
+
const folioID = parts[2]
|
|
44
|
+
const folioID2 = parts[4]
|
|
45
|
+
const left = parseFolioID(folioID)
|
|
46
|
+
const right = parseFolioID(folioID2)
|
|
47
|
+
return { left, right }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function SurfaceBrowser(props) {
|
|
51
|
+
const { db, open, toggleOpen } = props
|
|
52
|
+
const documents = useMemo(() => getData(db), [db])
|
|
53
|
+
const [pageCount, setPageCount] = useState({})
|
|
54
|
+
const [totalPages, setTotalPages] = useState(0)
|
|
55
|
+
const [tags, setTags] = useState([])
|
|
56
|
+
const [showFilters, setShowFilters] = useState(false)
|
|
57
|
+
|
|
58
|
+
const navigate = useNavigate()
|
|
59
|
+
const location = useLocation()
|
|
60
|
+
const selection = useMemo(() => getSelection(location.pathname), [location])
|
|
61
|
+
|
|
62
|
+
const navigateToSelection = (nextSelection) => {
|
|
63
|
+
const folioID = nextSelection?.left ? `${nextSelection.left.localID}_${nextSelection.left.surfaceID}` : null
|
|
64
|
+
const folioID2 = nextSelection?.right ? `${nextSelection.right.localID}_${nextSelection.right.surfaceID}` : null
|
|
65
|
+
const navParams = `/ec/${folioID || '-1'}/${folioID ? 'f' : 'g'}/${folioID2 || '-1'}/${folioID2 ? 'f' : 'g'}`
|
|
66
|
+
navigate(navParams + location.search)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const updatePageCount = (documentID, numPages) => {
|
|
70
|
+
const newCount = pageCount
|
|
71
|
+
newCount[documentID] = numPages
|
|
72
|
+
setPageCount(newCount)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
let p = 0
|
|
77
|
+
for (const key of Object.keys(pageCount)) {
|
|
78
|
+
p += pageCount[key]
|
|
79
|
+
}
|
|
80
|
+
setTotalPages(p)
|
|
81
|
+
}, [pageCount, tags])
|
|
82
|
+
|
|
83
|
+
const documentDetails = documents.map((doc) => {
|
|
84
|
+
return (
|
|
85
|
+
<DocumentDetail
|
|
86
|
+
key={`document-detail-${doc.id}`}
|
|
87
|
+
db={db}
|
|
88
|
+
documentName={doc.name}
|
|
89
|
+
documentLocalID={doc.local_id}
|
|
90
|
+
documentID={doc.id}
|
|
91
|
+
selection={selection}
|
|
92
|
+
navigateToSelection={navigateToSelection}
|
|
93
|
+
updatePageCount={count => updatePageCount(doc.id, count)}
|
|
94
|
+
tags={tags}
|
|
95
|
+
>
|
|
96
|
+
</DocumentDetail>
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<Collapse in={open} horizontal>
|
|
102
|
+
<Box
|
|
103
|
+
className="surface-browser"
|
|
104
|
+
>
|
|
105
|
+
<IconButton onClick={toggleOpen} className="surface-browser-close">
|
|
106
|
+
<ChevronLeftIcon />
|
|
107
|
+
</IconButton>
|
|
108
|
+
<Divider></Divider>
|
|
109
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
110
|
+
<Typography>Contents</Typography>
|
|
111
|
+
<Button
|
|
112
|
+
startIcon={<TuneIcon />}
|
|
113
|
+
onClick={() => setShowFilters(current => (!current))}
|
|
114
|
+
>
|
|
115
|
+
Filter
|
|
116
|
+
{ tags && tags.length
|
|
117
|
+
? (
|
|
118
|
+
<div style={{
|
|
119
|
+
fontSize: 'small',
|
|
120
|
+
backgroundColor: 'red',
|
|
121
|
+
borderRadius: '999px',
|
|
122
|
+
display: 'flex',
|
|
123
|
+
justifyContent: 'center',
|
|
124
|
+
alignItems: 'center',
|
|
125
|
+
padding: '3px',
|
|
126
|
+
color: 'white',
|
|
127
|
+
height: '16px',
|
|
128
|
+
width: '16px',
|
|
129
|
+
position: 'absolute',
|
|
130
|
+
top: '0',
|
|
131
|
+
left: '-12px',
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
{tags.length}
|
|
135
|
+
</div>
|
|
136
|
+
)
|
|
137
|
+
: null}
|
|
138
|
+
</Button>
|
|
139
|
+
</div>
|
|
140
|
+
<Typography>
|
|
141
|
+
{totalPages}
|
|
142
|
+
{' '}
|
|
143
|
+
Page
|
|
144
|
+
{ totalPages !== 1 ? 's' : ''}
|
|
145
|
+
</Typography>
|
|
146
|
+
{/* <ButtonGroup color="primary" aria-label="outlined primary button group">
|
|
147
|
+
<IconButton aria-label="grid">
|
|
148
|
+
<GridOnIcon></GridOnIcon>
|
|
149
|
+
</IconButton>
|
|
150
|
+
<IconButton aria-label="list">
|
|
151
|
+
<ListIcon></ListIcon>
|
|
152
|
+
</IconButton>
|
|
153
|
+
</ButtonGroup> */}
|
|
154
|
+
{ showFilters && (
|
|
155
|
+
<TagFilters
|
|
156
|
+
db={db}
|
|
157
|
+
filters={tags}
|
|
158
|
+
onToggleSelected={(tagId) => {
|
|
159
|
+
if (tags.includes(tagId)) {
|
|
160
|
+
setTags(current => (current.filter(t => (t !== tagId))))
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
setTags(current => ([...current, tagId]))
|
|
164
|
+
}
|
|
165
|
+
}}
|
|
166
|
+
/>
|
|
167
|
+
) }
|
|
168
|
+
<Box className="surface-browser-document-details">
|
|
169
|
+
{ documentDetails }
|
|
170
|
+
</Box>
|
|
171
|
+
</Box>
|
|
172
|
+
</Collapse>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export default SurfaceBrowser
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createTheme, ThemeProvider } from '@material-ui/core/styles'
|
|
2
|
+
import React, { useRef, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import HelpPopper from '../../EditionCrafter/component/HelpPopper'
|
|
5
|
+
import NarrowSidebar from './NarrowSidebar'
|
|
6
|
+
import SurfaceBrowser from './SurfaceBrowser'
|
|
7
|
+
|
|
8
|
+
function TagExploreSidebar(props) {
|
|
9
|
+
const { db } = props
|
|
10
|
+
const [openHelp, setOpenHelp] = useState(false)
|
|
11
|
+
const [openDrawer, setOpenDrawer] = useState(false)
|
|
12
|
+
|
|
13
|
+
const toggleHelp = () => {
|
|
14
|
+
setOpenHelp(!openHelp)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const toggleDrawer = () => {
|
|
18
|
+
setOpenDrawer(!openDrawer)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const helpRef = useRef(null)
|
|
22
|
+
const helpMarginStyle = { marginLeft: '60px', backgroundColor: '#242629', zIndex: '9999' }
|
|
23
|
+
const theme = createTheme({
|
|
24
|
+
palette: {
|
|
25
|
+
type: 'dark',
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="tag-explore-sidebar">
|
|
31
|
+
<ThemeProvider theme={theme}>
|
|
32
|
+
<NarrowSidebar
|
|
33
|
+
toggleDrawer={toggleDrawer}
|
|
34
|
+
toggleHelp={toggleHelp}
|
|
35
|
+
helpRef={helpRef}
|
|
36
|
+
>
|
|
37
|
+
</NarrowSidebar>
|
|
38
|
+
<SurfaceBrowser
|
|
39
|
+
db={db}
|
|
40
|
+
open={openDrawer}
|
|
41
|
+
toggleOpen={toggleDrawer}
|
|
42
|
+
>
|
|
43
|
+
</SurfaceBrowser>
|
|
44
|
+
<HelpPopper
|
|
45
|
+
marginStyle={helpMarginStyle}
|
|
46
|
+
anchorEl={helpRef.current}
|
|
47
|
+
open={openHelp}
|
|
48
|
+
onClose={toggleHelp}
|
|
49
|
+
/>
|
|
50
|
+
</ThemeProvider>
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default TagExploreSidebar
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Checkbox, FormControlLabel, FormGroup, Typography } from '@material-ui/core'
|
|
2
|
+
import { useContext, useEffect, useMemo, useState } from 'react'
|
|
3
|
+
import { getObjs } from '../../common/lib/sql'
|
|
4
|
+
import TagFilterContext from '../../EditionCrafter/context/TagFilterContext'
|
|
5
|
+
|
|
6
|
+
function getData(db) {
|
|
7
|
+
const taxonomiesStmt = db.prepare(`
|
|
8
|
+
SELECT
|
|
9
|
+
*
|
|
10
|
+
FROM
|
|
11
|
+
taxonomies;
|
|
12
|
+
`)
|
|
13
|
+
|
|
14
|
+
const tagsStmt = db.prepare(`
|
|
15
|
+
SELECT
|
|
16
|
+
tags.id AS id,
|
|
17
|
+
tags.name AS name,
|
|
18
|
+
tags.xml_id AS xml_id,
|
|
19
|
+
taxonomies.name as taxonomy,
|
|
20
|
+
taxonomies.id as taxonomy_id
|
|
21
|
+
FROM
|
|
22
|
+
tags
|
|
23
|
+
LEFT JOIN taxonomies
|
|
24
|
+
ON tags.taxonomy_id = taxonomies.id
|
|
25
|
+
INNER JOIN taggings
|
|
26
|
+
ON taggings.tag_id = tags.id
|
|
27
|
+
INNER JOIN elements
|
|
28
|
+
ON elements.id = taggings.element_id
|
|
29
|
+
WHERE
|
|
30
|
+
elements.type = 'zone'
|
|
31
|
+
GROUP BY
|
|
32
|
+
tags.xml_id`)
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
tags: getObjs(tagsStmt),
|
|
36
|
+
taxonomies: getObjs(taxonomiesStmt),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function TagFilters(props) {
|
|
41
|
+
const { onToggleSelected, filters } = props
|
|
42
|
+
const data = useMemo(() => getData(props.db), [props.db])
|
|
43
|
+
const [expanded, setExpanded] = useState(data.taxonomies?.map(() => (false)))
|
|
44
|
+
const [displayedTags, setDisplayedTags] = useState({})
|
|
45
|
+
|
|
46
|
+
const { toggleTag } = useContext(TagFilterContext)
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const tags = {}
|
|
50
|
+
for (let i = 0; i < data.taxonomies.length; i++) {
|
|
51
|
+
const tax = data.taxonomies[i]
|
|
52
|
+
const tagList = expanded[i] ? data.tags.filter(t => (t.taxonomy_id === tax.id)) : data.tags.filter(t => (t.taxonomy_id === tax.id))?.slice(0, 5)
|
|
53
|
+
tags[tax.id] = tagList
|
|
54
|
+
}
|
|
55
|
+
setDisplayedTags(tags)
|
|
56
|
+
}, [expanded, data])
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="tag-filters">
|
|
60
|
+
<div className="tag-list">
|
|
61
|
+
<FormGroup>
|
|
62
|
+
{ data.taxonomies.map((tax, idx) => {
|
|
63
|
+
const tagList = displayedTags[tax.id]
|
|
64
|
+
return (
|
|
65
|
+
<div key={tax.id}>
|
|
66
|
+
<Typography>{tax.name}</Typography>
|
|
67
|
+
<ul>
|
|
68
|
+
{ tagList?.map(tag => (
|
|
69
|
+
<FormControlLabel
|
|
70
|
+
as="li"
|
|
71
|
+
control={(
|
|
72
|
+
<Checkbox
|
|
73
|
+
checked={filters.includes(tag.id)}
|
|
74
|
+
onChange={() => {
|
|
75
|
+
onToggleSelected(tag.id)
|
|
76
|
+
toggleTag(tag.xml_id, 'left')
|
|
77
|
+
toggleTag(tag.xml_id, 'right')
|
|
78
|
+
}}
|
|
79
|
+
/>
|
|
80
|
+
)}
|
|
81
|
+
key={tag.id}
|
|
82
|
+
label={tag.name}
|
|
83
|
+
/>
|
|
84
|
+
))}
|
|
85
|
+
</ul>
|
|
86
|
+
<button
|
|
87
|
+
className="tag-filter-button"
|
|
88
|
+
type="button"
|
|
89
|
+
onClick={() => {
|
|
90
|
+
const newState = [...expanded]
|
|
91
|
+
newState[idx] = !expanded[idx]
|
|
92
|
+
setExpanded(newState)
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
{ !data.tags.filter(t => (t.taxonomy_id === tax.id))?.length || data.tags.filter(t => (t.taxonomy_id === tax.id)).length < 6 ? null : expanded[idx] ? 'Show less' : 'Show more'}
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
})}
|
|
100
|
+
</FormGroup>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default TagFilters
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { HashRouter } from 'react-router-dom'
|
|
3
|
+
import initSqlJs from 'sql.js'
|
|
4
|
+
import sqlJsInfo from 'sql.js/package.json'
|
|
5
|
+
import Loading from '../common/components/Loading'
|
|
6
|
+
import { getObjs } from '../common/lib/sql'
|
|
7
|
+
import EditionCrafter from '../EditionCrafter'
|
|
8
|
+
import TagFilterProvider from '../EditionCrafter/context/TagFilter'
|
|
9
|
+
import TagExploreSidebar from './components/TagExploreSidebar'
|
|
10
|
+
import './styles/base.css'
|
|
11
|
+
|
|
12
|
+
const initialFilters = {
|
|
13
|
+
categories: [],
|
|
14
|
+
tags: [],
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function initDb(url) {
|
|
18
|
+
const file = await fetch(url)
|
|
19
|
+
|
|
20
|
+
if (!file.ok) {
|
|
21
|
+
throw new Error('Failed fetching SQLite file.')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const buf = await file.arrayBuffer()
|
|
25
|
+
const arr = new Uint8Array(buf)
|
|
26
|
+
|
|
27
|
+
const db = await initSqlJs({
|
|
28
|
+
locateFile: file => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/${sqlJsInfo.version}/${file}`,
|
|
29
|
+
}).then((SQL) => {
|
|
30
|
+
const db = new SQL.Database(arr)
|
|
31
|
+
return db
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return db
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getData(db) {
|
|
38
|
+
const documentStmt = db.prepare(`
|
|
39
|
+
SELECT
|
|
40
|
+
documents.name AS name,
|
|
41
|
+
documents.local_id AS local_id
|
|
42
|
+
FROM
|
|
43
|
+
documents
|
|
44
|
+
`)
|
|
45
|
+
|
|
46
|
+
return getObjs(documentStmt)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function generateECProps(props, db) {
|
|
50
|
+
const documents = getData(db)
|
|
51
|
+
const { documentName, baseURL, transcriptionTypes } = props
|
|
52
|
+
const documentInfo = {}
|
|
53
|
+
|
|
54
|
+
for (const document of documents) {
|
|
55
|
+
documentInfo[document.local_id] = {
|
|
56
|
+
documentName: document.name,
|
|
57
|
+
transcriptionTypes,
|
|
58
|
+
iiifManifest: `${baseURL}/${document.local_id}/iiif/manifest.json`,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
documentName,
|
|
64
|
+
documentInfo,
|
|
65
|
+
tagExplorerMode: true,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function TagExplore(props) {
|
|
70
|
+
const [db, setDb] = useState(null)
|
|
71
|
+
const [ecProps, setECProps] = useState(null)
|
|
72
|
+
const [filters, setFilters] = useState(initialFilters)
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const loadDb = async () => {
|
|
76
|
+
const db = await initDb(props.dbUrl)
|
|
77
|
+
const ecProps = generateECProps(props, db)
|
|
78
|
+
setDb(db)
|
|
79
|
+
setECProps(ecProps)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!db) {
|
|
83
|
+
loadDb()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return () => {
|
|
87
|
+
if (db) {
|
|
88
|
+
db.close()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}, [props.dbUrl, db])
|
|
92
|
+
|
|
93
|
+
if (!db || !ecProps) {
|
|
94
|
+
return <Loading />
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className="tag-explore">
|
|
99
|
+
<HashRouter>
|
|
100
|
+
<TagFilterProvider>
|
|
101
|
+
<TagExploreSidebar db={db} />
|
|
102
|
+
<EditionCrafter {...ecProps} />
|
|
103
|
+
</TagFilterProvider>
|
|
104
|
+
</HashRouter>
|
|
105
|
+
</div>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default TagExplore
|