Skip to content

Commit bf41dee

Browse files
authored
Merge pull request #1719 from NatLibFi/issue1710-groups-view-basic-functionality
Implement basic functionality of groups view using Vue
2 parents 40ce410 + 1205cc3 commit bf41dee

4 files changed

Lines changed: 326 additions & 17 deletions

File tree

resource/css/skosmos.css

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -552,27 +552,32 @@ body {
552552
font-weight: bold !important;
553553
}
554554

555-
/*** sidebar hierarchy tab ***/
556-
#tab-hierarchy .sidebar-list .list-group-item {
555+
/*** sidebar hierarchy and groups tabs ***/
556+
#tab-hierarchy .sidebar-list .list-group-item,
557+
#tab-groups .sidebar-list .list-group-item {
557558
border-left: 1px dashed var(--vocab-text);
558559
border-radius: 0;
559560
white-space: nowrap;
560561
}
561562

562-
#tab-hierarchy .sidebar-list .list-group-item.top-concept {
563+
#tab-hierarchy .sidebar-list .list-group-item.top-concept,
564+
#tab-groups .sidebar-list .list-group-item.top-concept {
563565
border: none;
564566
}
565567

566-
#tab-hierarchy .sidebar-list .list-group-item .concept-label {
568+
#tab-hierarchy .sidebar-list .list-group-item .concept-label,
569+
#tab-groups .sidebar-list .list-group-item .concept-label {
567570
display: inline-block;
568571
border-left: 1px dashed var(--vocab-text);
569572
margin-left: 16px;
570573
padding-left: 14px;
574+
padding-right: 14px;
571575
white-space: wrap;
572576
}
573577

574578
/* horizontal line before concept */
575-
#tab-hierarchy .sidebar-list .list-group-item .concept-label::before {
579+
#tab-hierarchy .sidebar-list .list-group-item .concept-label::before,
580+
#tab-groups .sidebar-list .list-group-item .concept-label::before {
576581
content: '';
577582
position: absolute;
578583
left: 16px;
@@ -583,7 +588,8 @@ body {
583588
}
584589

585590
/* hiding bottom of vertical line on last concept of list */
586-
#tab-hierarchy .sidebar-list .list-group-item .concept-label.last::before {
591+
#tab-hierarchy .sidebar-list .list-group-item .concept-label.last::before,
592+
#tab-groups .sidebar-list .list-group-item .concept-label.last::before {
587593
top: auto;
588594
bottom: 0;
589595
height: calc(100% - 13px);
@@ -593,20 +599,24 @@ body {
593599
}
594600

595601
/* no border for children of the last concept */
596-
#tab-hierarchy .sidebar-list .list-group-item span.last + ul > li {
602+
#tab-hierarchy .sidebar-list .list-group-item span.last + ul > li,
603+
#tab-groups .sidebar-list .list-group-item span.last + ul > li {
597604
border: none;
598605
margin-left: 1px !important; /* add a 1px margin to offset missing border */
599606
}
600607

601-
#tab-hierarchy .sidebar-list .list-group-item a.selected {
608+
#tab-hierarchy .sidebar-list .list-group-item a.selected,
609+
#tab-groups .sidebar-list .list-group-item a.selected {
602610
color: var(--vocab-text) !important;
603611
}
604612

605-
#tab-hierarchy .sidebar-list .list-group-item .concept-notation {
613+
#tab-hierarchy .sidebar-list .list-group-item .concept-notation,
614+
#tab-groups .sidebar-list .list-group-item .concept-notation {
606615
color: var(--dark-color);
607616
}
608617

609-
#tab-hierarchy .sidebar-list .list-group-item a.selected .concept-notation {
618+
#tab-hierarchy .sidebar-list .list-group-item a.selected .concept-notation,
619+
#tab-groups .sidebar-list .list-group-item a.selected .concept-notation {
610620
font-weight: bold;
611621
}
612622

resource/js/tab-groups.js

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
/* global Vue */
2+
/* global partialPageLoad, getConceptURL */
3+
4+
const tabGroupsApp = Vue.createApp({
5+
data () {
6+
return {
7+
groups: [],
8+
selectedGroup: '',
9+
loadingGroups: true,
10+
loadingChildren: [],
11+
listStyle: {}
12+
}
13+
},
14+
provide () {
15+
return {
16+
partialPageLoad,
17+
getConceptURL,
18+
showNotation: window.SKOSMOS.showNotation
19+
}
20+
},
21+
mounted () {
22+
// Load groups if groups tab is active when the page is first opened (otherwise only load groups when the tab is clicked)
23+
if (document.querySelector('#groups > a').classList.contains('active')) {
24+
this.loadGroups()
25+
}
26+
},
27+
beforeUpdate () {
28+
this.setListStyle()
29+
},
30+
methods: {
31+
handleClickGroupsEvent () {
32+
// Only load groups if groups tab is available
33+
if (!document.querySelector('#groups > a').classList.contains('disabled')) {
34+
this.loadGroups()
35+
}
36+
},
37+
loadGroups () {
38+
this.loadingGroups = true
39+
fetch('rest/v1/' + window.SKOSMOS.vocab + '/groups/?lang=' + window.SKOSMOS.content_lang)
40+
.then(data => {
41+
return data.json()
42+
})
43+
.then(data => {
44+
console.log('groups data', data)
45+
46+
this.groups = []
47+
48+
const groups = data.groups
49+
const result = []
50+
51+
// Map groups by uri with group properties for easy lookup
52+
const uriMap = new Map()
53+
for (const group of groups) {
54+
uriMap.set(group.uri, { ...group, childGroups: [], isOpen: false, isGroup: true })
55+
}
56+
57+
// Iterate through groups and set child groups in uriMap
58+
for (const group of groups) {
59+
if (group.childGroups) {
60+
for (const childUri of group.childGroups) {
61+
const child = uriMap.get(childUri)
62+
if (child) {
63+
uriMap.get(group.uri).childGroups.push(child)
64+
}
65+
}
66+
}
67+
68+
// Add top level groups to result list
69+
if (!groups.some(other => other.childGroups?.includes(group.uri))) {
70+
result.push(uriMap.get(group.uri))
71+
}
72+
}
73+
74+
return { result, uriMap }
75+
})
76+
.then(({ result, uriMap }) => {
77+
// Check that we are on a group page
78+
if (uriMap.has(window.SKOSMOS.uri)) {
79+
this.selectedGroup = window.SKOSMOS.uri
80+
81+
// Only load members if selected group has members
82+
if (uriMap.get(this.selectedGroup).hasMembers) {
83+
fetch('rest/v1/' + window.SKOSMOS.vocab + '/groupMembers/?lang=' + window.SKOSMOS.content_lang + '&uri=' + this.selectedGroup)
84+
.then(data => {
85+
return data.json()
86+
})
87+
.then(data => {
88+
console.log('members data', data)
89+
// Filter out existing groups from members list and add the correct properties
90+
const members = data.members
91+
.filter(m => !uriMap.has(m.uri))
92+
.map(m => {
93+
return { ...m, childGroups: [], isOpen: false, isGroup: false }
94+
})
95+
96+
// Set isOpen to true for the selected group and its parents and add child members to selected group
97+
this.setIsOpenAndAddMembers(result, this.selectedGroup, members)
98+
99+
this.groups = result
100+
this.loadingGroups = false
101+
console.log('groups', this.groups)
102+
})
103+
} else {
104+
// If selected group has no members, set isOpen for the group and its parents
105+
this.setIsOpenAndAddMembers(result, this.selectedGroup, [])
106+
107+
this.groups = result
108+
this.loadingGroups = false
109+
console.log('groups', this.groups)
110+
}
111+
} else {
112+
// If we are on vocab home page, simply set groups to result
113+
this.groups = result
114+
this.loadingGroups = false
115+
console.log('groups', this.groups)
116+
}
117+
})
118+
},
119+
setIsOpenAndAddMembers (tree, selectedGroup, members) {
120+
// Recursive function to find selected group and set its properties
121+
const findAndSet = node => {
122+
if (node.uri === selectedGroup) {
123+
// If selected group was found, set this group to open, add members to it and return true
124+
node.isOpen = true
125+
node.childGroups.push(...members)
126+
return true
127+
}
128+
129+
for (const child of node.childGroups) {
130+
// Recursively call findAndSet for all children
131+
if (findAndSet(child)) {
132+
// If selected group was found in children, set this group to open and return true
133+
node.isOpen = true
134+
return true
135+
}
136+
}
137+
138+
// If selected group was not found, return false
139+
return false
140+
}
141+
142+
for (const root of tree) {
143+
findAndSet(root)
144+
}
145+
},
146+
setListStyle () {
147+
// TODO: set list style when mounting component and resizing window
148+
},
149+
loadChildren (group) {
150+
// Load children only if group has children and they have not been loaded yet
151+
if (group.childGroups.length === 0 && group.hasMembers) {
152+
this.loadingChildren.push(group)
153+
fetch('rest/v1/' + window.SKOSMOS.vocab + '/groupMembers/?lang=' + window.SKOSMOS.content_lang + '&uri=' + group.uri)
154+
.then(data => {
155+
return data.json()
156+
})
157+
.then(data => {
158+
console.log('data', data)
159+
for (const m of data.members) {
160+
group.childGroups.push({ ...m, childGroups: [], isOpen: false, isGroup: false })
161+
}
162+
this.loadingChildren = this.loadingChildren.filter(x => x !== group)
163+
console.log('groups', this.groups)
164+
})
165+
}
166+
}
167+
},
168+
template: `
169+
<div v-click-tab-groups="handleClickGroupsEvent" v-resize-window="setListStyle">
170+
<div id="groups-list" class="sidebar-list p-0" :style="listStyle">
171+
<ul v-if="!loadingGroups" class="list-group">
172+
<tab-groups-wrapper
173+
:groups="groups"
174+
:selectedGroup="selectedGroup"
175+
:loadingChildren="loadingChildren"
176+
@load-children="loadChildren($event)"
177+
@select-group="selectedGroup = $event"
178+
></tab-groups-wrapper>
179+
</ul>
180+
<i v-else class="fa-solid fa-spinner fa-spin-pulse"></i>
181+
</div>
182+
</div>
183+
`
184+
})
185+
186+
/* Custom directive used to add an event listener on clicks on the groups nav-item element */
187+
tabGroupsApp.directive('click-tab-groups', {
188+
beforeMount: (el, binding) => {
189+
el.clickTabEvent = event => {
190+
binding.value() // calling the method given as the attribute value (handleClickGroupsEvent)
191+
}
192+
document.querySelector('#groups').addEventListener('click', el.clickTabEvent) // registering an event listener on clicks on the groups nav-item element
193+
},
194+
unmounted: el => {
195+
document.querySelector('#groups').removeEventListener('click', el.clickTabEvent)
196+
}
197+
})
198+
199+
/* Custom directive used to add an event listener on resizing the window */
200+
tabGroupsApp.directive('resize-window', {
201+
beforeMount: (el, binding) => {
202+
el.resizeWindowEvent = event => {
203+
binding.value() // calling the method given as the attribute value (setListStyle)
204+
}
205+
window.addEventListener('resize', el.resizeWindowEvent) // registering an event listener on resizing the window
206+
},
207+
unmounted: el => {
208+
window.removeEventListener('resize', el.resizeWindowEvent)
209+
}
210+
})
211+
212+
tabGroupsApp.component('tab-groups-wrapper', {
213+
props: ['groups', 'selectedGroup', 'loadingChildren'],
214+
emits: ['loadChildren', 'selectGroup'],
215+
mounted () {
216+
},
217+
methods: {
218+
loadChildren (group) {
219+
this.$emit('loadChildren', group)
220+
},
221+
selectGroup (group) {
222+
this.$emit('selectGroup', group)
223+
}
224+
},
225+
template: `
226+
<template v-for="(g, i) in groups" >
227+
<tab-groups
228+
:group="g"
229+
:selectedGroup="selectedGroup"
230+
:isTopGroup="true"
231+
:isLast="i == groups.length - 1"
232+
:loadingChildren="loadingChildren"
233+
@load-children="loadChildren($event)"
234+
@select-group="selectGroup($event)"
235+
></tab-groups>
236+
</template>
237+
`
238+
})
239+
240+
tabGroupsApp.component('tab-groups', {
241+
props: ['group', 'selectedGroup', 'isTopGroup', 'isLast', 'loadingChildren'],
242+
emits: ['loadChildren', 'selectGroup'],
243+
inject: ['partialPageLoad', 'getConceptURL', 'showNotation'],
244+
methods: {
245+
handleClickOpenEvent (group) {
246+
group.isOpen = !group.isOpen
247+
this.$emit('loadChildren', group)
248+
},
249+
handleClickGroupEvent (event, group) {
250+
group.isOpen = true
251+
this.$emit('loadChildren', group)
252+
this.$emit('selectGroup', group.uri)
253+
this.partialPageLoad(event, this.getConceptURL(group.uri))
254+
},
255+
loadChildrenRecursive (group) {
256+
this.$emit('loadChildren', group)
257+
},
258+
selectGroupRecursive (group) {
259+
this.$emit('selectGroup', group)
260+
}
261+
},
262+
template: `
263+
<li class="list-group-item p-0" :class="{ 'top-concept': isTopGroup }">
264+
<button type="button" class="hierarchy-button btn btn-primary" aria-label="Open"
265+
:class="{ 'open': group.isOpen }"
266+
v-if="group.hasMembers"
267+
@click="handleClickOpenEvent(group)"
268+
>
269+
<template v-if="loadingChildren.includes(group)">
270+
<i class="fa-solid fa-spinner fa-spin-pulse"></i>
271+
</template>
272+
<template v-else>
273+
<img v-if="group.isOpen" alt="" src="resource/pics/black-lower-right-triangle.png">
274+
<img v-else alt="" src="resource/pics/lower-right-triangle.png">
275+
</template>
276+
</button>
277+
<span class="concept-label" :class="{ 'last': isLast }">
278+
<a :class="{ 'selected': selectedGroup === group.uri }"
279+
:href="getConceptURL(group.uri)"
280+
@click="handleClickGroupEvent($event, group)"
281+
aria-label="Go to the concept page"
282+
>
283+
<span v-if="showNotation && group.notation" class="concept-notation">{{ group.notation }} </span>
284+
{{ group.prefLabel }}
285+
</a>
286+
</span>
287+
<ul class="list-group ps-3" v-if="group.childGroups.length !== 0 && group.isOpen">
288+
<template v-for="(g, i) in group.childGroups">
289+
<tab-groups
290+
:group="g"
291+
:selectedGroup="selectedGroup"
292+
:isTopGroup="false"
293+
:isLast="i == group.childGroups.length - 1"
294+
:loadingChildren="loadingChildren"
295+
@load-children="loadChildrenRecursive($event)"
296+
@select-group="selectGroupRecursive($event)"
297+
></tab-groups>
298+
</template>
299+
</ul>
300+
</li>
301+
`
302+
})
303+
304+
tabGroupsApp.mount('#tab-groups')

src/controller/WebController.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -478,13 +478,7 @@ public function invokeVocabularyHome($request)
478478
$vocab = $request->getVocab();
479479

480480
$defaultView = $vocab->getConfig()->getDefaultSidebarView();
481-
// load template
482-
if ($defaultView === 'groups') {
483-
$this->invokeGroupIndex($request, true);
484-
return;
485-
} elseif ($defaultView === 'new') {
486-
$this->invokeChangeList($request);
487-
}
481+
488482
$pluginParameters = json_encode($vocab->getConfig()->getPluginParameters());
489483

490484
$template = $this->twig->load('vocab-home.twig');

0 commit comments

Comments
 (0)