Skip to content

Commit 40ce410

Browse files
authored
Merge pull request #1718 from NatLibFi/issue1714-concept-page-skos-xl
SKOS XL popup on concept page
2 parents 30d6246 + b866579 commit 40ce410

5 files changed

Lines changed: 295 additions & 139 deletions

File tree

resource/css/skosmos.css

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
--vocab-table-border: var(--medium-color);
3131
--vocab-link: var(--secondary-dark-color);
3232
--vocab-box-text: var(--dark-color);
33+
--tooltip-fg-color: var(--light-color);
34+
--tooltip-bg-color: var(--dark-color);
35+
--tooltip-border-color: var(--secondary-medium-color);
3336

3437
/* Font definitions */
3538
--font-size: 14px;
@@ -951,3 +954,83 @@ body {
951954
font-size: 20px !important;
952955
}
953956

957+
/* Tooltips */
958+
959+
.tooltip-html {
960+
position: relative;
961+
display: inline-block;
962+
cursor: pointer;
963+
}
964+
965+
#concept-label .tooltip-html {
966+
bottom: 4px;
967+
}
968+
969+
.tooltip-html button {
970+
line-height: 1.0;
971+
padding: 3px;
972+
margin: -3px;
973+
}
974+
975+
.tooltip-html button:focus {
976+
border: 2px solid var(--vocab-text);
977+
border-radius: 0;
978+
margin: -4px;
979+
}
980+
981+
.tooltip-html:hover .tooltip-html-content, .tooltip-html:focus-within .tooltip-html-content {
982+
visibility: visible;
983+
opacity: 1;
984+
}
985+
986+
.tooltip-html-content {
987+
box-sizing: border-box;
988+
position: absolute;
989+
top: 20px;
990+
left: 0;
991+
display: block;
992+
cursor: text;
993+
width: 400px;
994+
color: var(--tooltip-fg-color);
995+
background-color: var(--tooltip-bg-color);
996+
border: 3px solid var(--tooltip-border-color);
997+
z-index: 9001;
998+
visibility: hidden;
999+
opacity: 0;
1000+
padding: 0.25rem 0.5rem;
1001+
}
1002+
1003+
/* The triangle that appears near the element that triggered it. */
1004+
.tooltip-html-content:after {
1005+
position: absolute;
1006+
content: '';
1007+
width: 10px;
1008+
height: 10px;
1009+
transform: rotate(45deg);
1010+
top: -7px;
1011+
left: 0;
1012+
margin-left: 2px;
1013+
color: var(--tooltip-fg-color);
1014+
border-top: 6px solid var(--tooltip-border-color);
1015+
border-right: 6px solid transparent;
1016+
border-bottom: 6px solid transparent;
1017+
border-left: 6px solid var(--tooltip-border-color);
1018+
z-index: 9002;
1019+
}
1020+
1021+
/* Definition lists used in tooltips. */
1022+
1023+
dl.tooltip-html-content dt {
1024+
display: block;
1025+
float: left;
1026+
font-weight: bold;
1027+
margin-right: 0.4em;
1028+
}
1029+
1030+
dl.tooltip-html-content dt::after {
1031+
content: ":";
1032+
}
1033+
1034+
dl.tooltip-html-content dd {
1035+
display: block;
1036+
}

src/view/concept-card.inc.twig

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,13 @@
3636
{%- endif -%}
3737
</div>
3838
<div class="col-sm-8" id="concept-label">
39+
{% if concept.hasXlLabel %}
40+
{% set descriptionId = 'concept-preflabel-xl' %}
41+
{{ include('xl-label.inc.twig', {xlLabel: concept.xlLabel, xlDescriptionId: descriptionId}) }}
42+
{% endif %}
3943
{% if concept.notation %}
4044
<h1 id="concept-preflabel"
45+
{% if descriptionId %}aria-describedby="{{ descriptionId }}"{% endif %}
4146
class="mb-0">
4247
<span class="notation user-select-all" id="concept-notation">{{ concept.notation }}</span> {{ concept.label }}</h1><button
4348
class="btn btn-default copy-clipboard px-1" type="button" id="copy-notation"
@@ -46,6 +51,7 @@
4651
</button>
4752
{% else %}
4853
<h1 id="concept-preflabel"
54+
{% if descriptionId %}aria-describedby="{{ descriptionId }}"{% endif %}
4955
class="mb-0 user-select-all">{{ concept.label }}</h1><button
5056
class="btn btn-default copy-clipboard px-1" type="button" id="copy-preflabel"
5157
data-bs-toggle="tooltip" data-bs-placement="button" title="{{ 'Copy to clipboard' | trans }}">
@@ -78,7 +84,17 @@
7884
</a>
7985
</li>
8086
{% else %} {# literals, e.g. altLabels #}
81-
<li>{% if propval.containsHtml %}{{ propval.label|raw }}{% else %}{{ propval.label }}{% endif %}</li>
87+
<li>
88+
{%- apply spaceless %}
89+
{% if propval.hasXlProperties %}
90+
{% set descriptionId = "concept-property-xl-#{loop.parent.loop.index}-#{loop.index}" %}
91+
{{ include('xl-label.inc.twig', {xlLabel: propval.xlLabel, xlDescriptionId: descriptionId}) }}
92+
{% else %}
93+
{% set descriptionId = '' %}
94+
{% endif %}
95+
<span{% if descriptionId %} aria-describedby="{{ descriptionId }}"{% endif %}>{% if propval.containsHtml %}{{ propval.label|raw }}{% else %}{{ propval.label }}{% endif %}</span>
96+
{% endapply -%}
97+
</li>
8298
{% endif %}
8399
{% endfor %}
84100
</ul>
@@ -96,14 +112,21 @@
96112
<div class="col-sm-5">
97113
<ul>
98114
{% for value in labels.prefLabel|default([])|merge(labels.altLabel|default([])) %}
99-
{% if value.type == "skos:prefLabel" and value.lang in request.vocab.config.languages %}
100115
<li>
101-
<a href="{{ concept.uri|link_url(request.vocabid,request.lang, 'page', value.lang) }}"
102-
hreflang="{{ value.lang }}">{{ value.label }}</a>
103-
</li>
116+
{% if value.hasXlProperties %}
117+
{% set descriptionId = "concept-property-xl-#{loop.parent.loop.index}-#{loop.index}" %}
118+
{{ include('xl-label.inc.twig', {xlLabel: value.xlLabel, xlDescriptionId: descriptionId}) }}
104119
{% else %}
105-
<li class="altlabel">{{ value.label }}</li>
120+
{% set descriptionId = '' %}
106121
{% endif %}
122+
{% if value.type == "skos:prefLabel" and value.lang in request.vocab.config.languages %}
123+
<a {% if descriptionId %}aria-describedby="{{ descriptionId }}"{% endif %}
124+
href="{{ concept.uri|link_url(request.vocabid,request.lang, 'page', value.lang) }}"
125+
hreflang="{{ value.lang }}">{{ value.label }}</a>
126+
{% else %}
127+
<span class="altlabel"{% if descriptionId %} aria-describedby="{{ descriptionId }}"{% endif %}>{{ value.label }}</span>
128+
{% endif %}
129+
</li>
107130
{% endfor %}
108131
</ul>
109132
</div>
@@ -133,7 +156,7 @@
133156
<a class="me-3" href="rest/v1/{{ vocab.id }}/data?uri={{ concept.uri|url_encode }}&amp;format=application/rdf%2Bxml">RDF/XML</a>
134157
</li>
135158
<li>
136-
<a class="me-3"href="rest/v1/{{ vocab.id }}/data?uri={{ concept.uri|url_encode }}&amp;format=text/turtle">TURTLE</a>
159+
<a class="me-3" href="rest/v1/{{ vocab.id }}/data?uri={{ concept.uri|url_encode }}&amp;format=text/turtle">TURTLE</a>
137160
</li>
138161
<li>
139162
<a href="rest/v1/{{ vocab.id }}/data?uri={{ concept.uri|url_encode }}&amp;format=application/ld%2Bjson">JSON-LD</a>

src/view/xl-label.inc.twig

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<div class="tooltip-html">
2+
<button class="btn" aria-label="{{ 'More information about this term'|trans }}" aria-controls="{{ xlDescriptionId }}"><i class="fa-solid fa-circle-info"></i></button>
3+
<dl class="tooltip-html-content" id="{{ xlDescriptionId }}">
4+
{% for key, val in xlLabel.properties %}
5+
<dt>{{ key|trans }}</dt>
6+
<dd>{{ val }}</dd>
7+
{% endfor %}
8+
</dl>
9+
</div>
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
describe('Concept page, full vs. partial page loads', () => {
2+
const pageLoadTypes = ["full", "partial"]
3+
4+
// tests that should be executed both with and without partial page load
5+
pageLoadTypes.forEach((pageLoadType) => {
6+
it('contains concept preflabel / ' + pageLoadType, () => {
7+
if (pageLoadType == "full") {
8+
cy.visit('/yso/en/page/p39473') // go to "burial mounds" concept page
9+
} else {
10+
cy.visit('/yso/en/page/p5714') // go to "prehistoric graves" concept page
11+
// click on the link to "burial mounds" to trigger partial page load
12+
cy.get('#tab-hierarchy').contains('a', 'burial mounds').click()
13+
}
14+
15+
// check that the vocabulary title is correct
16+
cy.get('#vocab-title > a').invoke('text').should('equal', 'YSO - General Finnish ontology (archaeology)')
17+
18+
// check the concept prefLabel
19+
cy.get('#concept-heading h1').invoke('text').should('equal', 'burial mounds')
20+
})
21+
it('concept preflabel can be copied to clipboard / ' + pageLoadType, () => {
22+
if (pageLoadType == "full") {
23+
cy.visit('/yso/en/page/p39473') // go to "burial mounds" concept page
24+
} else {
25+
cy.visit('/yso/en/page/p5714') // go to "prehistoric graves" concept page
26+
// click on the link to "burial mounds" to trigger partial page load
27+
cy.get('#tab-hierarchy').contains('a', 'burial mounds').click()
28+
}
29+
30+
// click the copy to clipboard button next to the prefLabel
31+
cy.get('#copy-preflabel').click()
32+
33+
// check that the clipboard now contains "music pyramids"
34+
// NOTE: This test may fail when running Cypress interactively in a browser.
35+
// The reason is browser security policies for accessing the clipboard.
36+
// If that happens, make sure the browser window has focus and re-run the test.
37+
cy.window().its('navigator.clipboard').invoke('readText').then((result) => {}).should('equal', 'burial mounds');
38+
})
39+
it('contains concept URI / ' + pageLoadType, () => {
40+
if (pageLoadType == "full") {
41+
cy.visit('/yso/en/page/p39473') // go to "burial mounds" concept page
42+
} else {
43+
cy.visit('/yso/en/page/p5714') // go to "prehistoric graves" concept page
44+
// click on the link to "burial mounds" to trigger partial page load
45+
cy.get('#tab-hierarchy').contains('a', 'burial mounds').click()
46+
}
47+
48+
// check the property name
49+
cy.get('.prop-uri .property-label').invoke('text').should('equal', 'URI')
50+
51+
// check the concept URI
52+
cy.get('#concept-uri').invoke('text').should('equal', 'http://www.yso.fi/onto/yso/p39473')
53+
})
54+
it('concept URI can be copied to clipboard / ' + pageLoadType, () => {
55+
if (pageLoadType == "full") {
56+
cy.visit('/yso/en/page/p39473') // go to "burial mounds" concept page
57+
} else {
58+
cy.visit('/yso/en/page/p5714') // go to "prehistoric graves" concept page
59+
// click on the link to "burial mounds" to trigger partial page load
60+
cy.get('#tab-hierarchy').contains('a', 'burial mounds').click()
61+
}
62+
63+
// click the copy to clipboard button next to the URI
64+
cy.get('#copy-uri').click()
65+
66+
// check that the clipboard now contains "http://www.yso.fi/onto/yso/p39473"
67+
// NOTE: This test may fail when running Cypress interactively in a browser.
68+
// The reason is browser security policies for accessing the clipboard.
69+
// If that happens, make sure the browser window has focus and re-run the test.
70+
cy.window().its('navigator.clipboard').invoke('readText').then((result) => {}).should('equal', 'http://www.yso.fi/onto/yso/p39473');
71+
})
72+
it('concept notation can be copied to clipboard / ' + pageLoadType, () => {
73+
if (pageLoadType == "full") {
74+
cy.visit('/test-notation-sort/en/page/?uri=http%3A%2F%2Fwww.skosmos.skos%2Ftest%2Fta0115') // go to "Eel" concept page
75+
} else {
76+
cy.visit('/test-notation-sort/en/page/?uri=http%3A%2F%2Fwww.skosmos.skos%2Ftest%2Fta0114') // go to "Buri" concept page
77+
// click on the link to "Eel" to trigger partial page load
78+
cy.get('#tab-hierarchy').contains('a', 'Eel').click()
79+
}
80+
81+
// click the copy to clipboard button next to the URI
82+
cy.get('#copy-notation').click()
83+
84+
// check that the clipboard now contains "33.2"
85+
// NOTE: This test may fail when running Cypress interactively in a browser.
86+
// The reason is browser security policies for accessing the clipboard.
87+
// If that happens, make sure the browser window has focus and re-run the test.
88+
cy.window().its('navigator.clipboard').invoke('readText').then((result) => {}).should('equal', '33.2');
89+
})
90+
it('contains mappings / ' + pageLoadType, () => {
91+
if (pageLoadType == "full") {
92+
cy.visit('/yso/en/page/p14174') // go to "labyrinths" concept page
93+
} else {
94+
cy.visit('/yso/en/page/p5714') // go to "prehistoric graves" concept page
95+
// click on the link to "labyrinths" to trigger partial page load
96+
cy.get('#tab-hierarchy').contains('a', 'labyrinths').click()
97+
}
98+
99+
// check that we have some mappings
100+
cy.get('#concept-mappings').should('not.be.empty')
101+
// check that spinner does not exist after load
102+
cy.get('#concept-mappings i.fa-spinner', {'timeout': 15000}).should('not.exist')
103+
104+
// check the first mapping property name
105+
// NOTE: we need to increase the timeout as the mappings can take a long time to load
106+
cy.get('.prop-mapping h2', {'timeout': 20000}).eq(0).contains('Closely matching concepts')
107+
// check the first mapping property values
108+
cy.get('.prop-mapping').eq(0).find('.prop-mapping-label').eq(0).contains('Labyrinths')
109+
cy.get('.prop-mapping').eq(0).find('.prop-mapping-label').eq(0).find('a').should('have.attr', 'href', 'http://id.loc.gov/authorities/subjects/sh85073793')
110+
cy.get('.prop-mapping').eq(0).find('.prop-mapping-vocab').eq(0).contains('Library of Congress Subject Headings')
111+
// check that the first mapping property has the right number of entries
112+
cy.get('.prop-mapping').eq(0).find('.prop-mapping-label').should('have.length', 1)
113+
114+
// check the second mapping property name
115+
cy.get('.prop-mapping h2').eq(1).contains('Exactly matching concepts')
116+
// check the second mapping property values
117+
cy.get('.prop-mapping').eq(1).find('.prop-mapping-label').eq(0).contains('labyrinter (sv)')
118+
cy.get('.prop-mapping').eq(1).find('.prop-mapping-label').eq(0).find('a').invoke('text').should('equal', 'labyrinter')
119+
cy.get('.prop-mapping').eq(1).find('.prop-mapping-label').eq(0).find('a').should('have.attr', 'href', 'http://www.yso.fi/onto/allars/Y21700')
120+
cy.get('.prop-mapping').eq(1).find('.prop-mapping-vocab').eq(0).contains('Allärs - General thesaurus in Swedish')
121+
// skipping the middle one (mapping to KOKO concept) as it's similar to the others
122+
cy.get('.prop-mapping').eq(1).find('.prop-mapping-label').eq(2).contains('labyrintit (fi)')
123+
cy.get('.prop-mapping').eq(1).find('.prop-mapping-label').eq(2).find('a').invoke('text').should('equal', 'labyrintit')
124+
cy.get('.prop-mapping').eq(1).find('.prop-mapping-label').eq(2).find('a').should('have.attr', 'href', 'http://www.yso.fi/onto/ysa/Y108389')
125+
cy.get('.prop-mapping').eq(1).find('.prop-mapping-vocab').eq(2).contains('YSA - Yleinen suomalainen asiasanasto')
126+
// check that the second mapping property has the right number of entries
127+
cy.get('.prop-mapping').eq(1).find('.prop-mapping-label').should('have.length', 3)
128+
})
129+
130+
});
131+
})

0 commit comments

Comments
 (0)