Commit eeb63dc7 authored by Wolfgang's avatar Wolfgang

Add facets to the app using a sample taxonomy.

parent 967842a9
......@@ -3,17 +3,21 @@
<fulltext default="none" attributes="false"/>
<lucene>
<module uri="http://teipublisher.com/index" prefix="nav" at="index.xql"/>
<text qname="tei:body">
<text match="/tei:TEI/tei:text">
<ignore qname="tei:div"/>
<field name="title" expression="nav:get-metadata(ancestor::tei:TEI, 'title')"/>
<field name="author" expression="nav:get-metadata(ancestor::tei:TEI, 'author')"/>
<field name="language" expression="nav:get-metadata(ancestor::tei:TEI, 'language')"/>
<field name="date" expression="nav:get-metadata(ancestor::tei:TEI, 'date')"/>
<field name="file" expression="util:document-name(.)"/>
<facet dimension="genre" expression="nav:get-metadata(ancestor::tei:TEI, 'genre')" hierarchical="yes"/>
<facet dimension="language" expression="nav:get-metadata(ancestor::tei:TEI, 'language')"/>
</text>
<text qname="tei:div">
<!-- exclude nested divs which are indexed separately -->
<ignore qname="tei:div"/>
<facet dimension="genre" expression="nav:get-metadata(ancestor::tei:TEI, 'genre')" hierarchical="yes"/>
<facet dimension="language" expression="nav:get-metadata(ancestor::tei:TEI, 'language')"/>
</text>
<text qname="tei:head"/>
<text match="//tei:titleStmt/tei:title"/>
......@@ -21,6 +25,8 @@
<!--text match="//tei:sourceDesc/tei:biblFull/tei:titleStmt/tei:author"/-->
<text qname="dbk:section">
<field name="title" expression="nav:get-metadata(ancestor::dbk:article, 'title')"/>
<facet dimension="genre" expression="nav:get-metadata(ancestor::dbk:article, 'genre')" hierarchical="yes"/>
<facet dimension="language" expression="nav:get-metadata(ancestor::dbk:article, 'language')"/>
</text>
<text qname="dbk:title"/>
</lucene>
......
......@@ -45,6 +45,10 @@
.toolbar pb-search {
padding-left: 0;
}
#facets {
display: none;
}
}
.tp-title {
......@@ -97,8 +101,17 @@
};
}
.panels .doclist .card-content {
display: flex;
flex-direction: row;
justify-content: space-between;
}
#document-list {
flex: 2;
}
aside {
flex: 1 0 auto;
display: flex;
flex-direction: column;
max-width: 480px;
......@@ -168,6 +181,43 @@
font-size: 16px;
margin: 0;
}
#facets {
flex: 0;
min-width: 220px;
margin-right: 20px;
}
#facets h3 {
display: flex;
flex-direction: row;
align-items: center;
}
#facets h3 paper-checkbox {
margin-left: 16px;
font-size: 11px;
}
#facets table {
font-size: 14px;
}
#facets table table {
padding-left: 16px;
width: 100%;
}
#facets table td{
padding:4px 0;
}
#facets table td:nth-child(2) {
color: #808080;
text-align: right;
padding-left: 1em;
vertical-align: middle;
}
</style>
</custom-style>
</head>
......@@ -288,13 +338,16 @@
<section class="panels">
<paper-card class="doclist" heading="i18n(documents)">
<div class="card-content">
<pb-custom-form id="facets" url="modules/facets.xql" event="pb-results-received" subscribe="docs" emit="docs"/>
<pb-browse-docs id="document-list" url="templates/documents.html" sort-label="i18n(sort-by)"
sort-options='[{"label": "Title", "value": "title"},{"label": "Author", "value": "author"},{"label": "Modification Date", "value": "default"}]'
sort-by="title"
filter-options='[{"label": "Title", "value": "title"},{"label": "Author", "value": "author"},{"label": "File name", "value": "file"}]'
filter-by="title"
auto="auto" history="history" login="login">
<pb-paginate slot="toolbar" id="paginate" per-page="10" range="5" found-label="i18n(browsing)"></pb-paginate>
auto="auto" history="history" login="login"
emit="docs" subscribe="docs">
<pb-paginate slot="toolbar" id="paginate" per-page="10" range="5" found-label="i18n(browsing)"
emit="docs" subscribe="docs"></pb-paginate>
</pb-browse-docs>
</div>
</paper-card>
......@@ -349,5 +402,24 @@
</div>
</paper-dialog>
</pb-page>
<script>
const facets = document.getElementById('facets');
facets.addEventListener('pb-custom-form-loaded', function(ev) {
const elems = ev.detail.querySelectorAll('.facet');
elems.forEach(facet => {
facet.addEventListener('change', () => {
const table = facet.closest('table');
if (table) {
const nested = table.querySelectorAll('.nested .facet').forEach(nested => {
if (nested != facet) {
nested.checked = false;
}
});
}
facets.submit();
});
});
});
</script>
</body>
</html>
......@@ -3,7 +3,26 @@ xquery version "3.1";
module namespace idx="http://teipublisher.com/index";
declare namespace tei="http://www.tei-c.org/ns/1.0";
declare namespace dbk="http://docbook.org/ns/docbook";
declare variable $idx:app-root :=
let $rawPath := system:get-module-load-path()
return
(: strip the xmldb: part :)
if (starts-with($rawPath, "xmldb:exist://")) then
if (starts-with($rawPath, "xmldb:exist://embedded-eXist-server")) then
substring($rawPath, 36)
else
substring($rawPath, 15)
else
$rawPath
;
(:~
: Helper function called from collection.xconf to create index fields and facets.
: This module needs to be loaded before collection.xconf starts indexing documents
: and therefore should reside in the root of the app.
:)
declare function idx:get-metadata($root as element(), $field as xs:string) {
let $header := $root/tei:teiHeader
return
......@@ -11,18 +30,37 @@ declare function idx:get-metadata($root as element(), $field as xs:string) {
case "title" return
string-join((
$header//tei:msDesc/tei:head, $header//tei:titleStmt/tei:title[@type = 'main'],
$header//tei:titleStmt/tei:title
$header//tei:titleStmt/tei:title,
$root/dbk:info/dbk:title
), " - ")
case "author" return (
$header//tei:correspDesc/tei:correspAction/tei:persName,
$header//tei:titleStmt/tei:author
)
case "language" return
($root/@xml:lang/string(), $header/@xml:lang/string(), "en")[1]
case "date" return (
head((
$header//tei:langUsage/tei:language/@ident,
$root/@xml:lang,
$header/@xml:lang
))
case "date" return head((
$header//tei:correspDesc/tei:correspAction/tei:date/@when,
$header//tei:sourceDesc/(tei:bibl|tei:biblFull)/tei:publicationStmt/tei:date,
$header//tei:sourceDesc/(tei:bibl|tei:biblFull)/tei:date/@when,
$header//tei:fileDesc/tei:editionStmt/tei:edition/tei:date,
$header//tei:publicationStmt/tei:date
)[1]
))
case "genre" return (
idx:get-genre($header),
$root/dbk:info/dbk:keywordset[@vocab="#genre"]/dbk:keyword
)
default return
()
};
declare function idx:get-genre($header as element()?) {
for $target in $header//tei:textClass/tei:catRef[@scheme="#genre"]/@target
let $category := id(substring($target, 2), doc($idx:app-root || "/data/taxonomy.xml"))
return
$category/ancestor-or-self::tei:category[parent::tei:category]/tei:catDesc
};
......@@ -42,9 +42,9 @@ declare variable $config:default-view := "div";
declare variable $config:default-template := "view.html";
(:
: The element to search by default, either 'tei:div' or 'tei:body'.
: The element to search by default, either 'tei:div' or 'tei:text'.
:)
declare variable $config:search-default := "tei:div";
declare variable $config:search-default := "tei:text";
(:
: Defines which nested divs will be displayed as single units on one
......@@ -65,6 +65,34 @@ declare variable $config:pagination-depth := 10;
:)
declare variable $config:pagination-fill := 5;
(:
: Display configuration for facets to be shown in the sidebar. The facets themselves
: are configured in the index configuration, collection.xconf.
:)
declare variable $config:facets := [
map {
"dimension": "genre",
"heading": "Genre",
"max": 5,
"hierarchical": true()
},
map {
"dimension": "language",
"heading": "Language",
"max": 5,
"hierarchical": false(),
"output": function($label) {
switch($label)
case "de" return "German"
case "es" return "Spanish"
case "la" return "Latin"
case "fr" return "French"
case "en" return "English"
default return $label
}
}
];
(:
: The function to be called to determine the next content chunk to display.
: It takes two parameters:
......@@ -212,7 +240,7 @@ declare variable $config:data-root := $config:app-root || "/data";
declare variable $config:data-default := $config:data-root || "/test";
declare variable $config:data-exclude := "/doc(/blog)?";
declare variable $config:data-exclude := "(taxonomy.xml|/doc(/blog)?)";
declare variable $config:default-odd := "teipublisher.odd";
......
......@@ -24,11 +24,10 @@ declare namespace db="http://docbook.org/ns/docbook";
import module namespace config="http://www.tei-c.org/tei-simple/config" at "config.xqm";
import module namespace nav="http://www.tei-c.org/tei-simple/navigation/docbook" at "navigation-dbk.xql";
declare variable $dbs:QUERY_OPTIONS :=
<options>
<leading-wildcard>yes</leading-wildcard>
<filter-rewrite>yes</filter-rewrite>
</options>;
declare variable $dbs:QUERY_OPTIONS := map {
"leading-wildcard": "yes",
"filter-rewrite": "yes"
};
declare function dbs:query-default($fields as xs:string+, $query as xs:string, $target-texts as xs:string*) {
if(string($query)) then
......@@ -39,21 +38,38 @@ declare function dbs:query-default($fields as xs:string+, $query as xs:string, $
if ($target-texts) then
for $text in $target-texts
return
$config:data-root ! doc(. || "/" || $text)//db:title[ft:query(., $query, $dbs:QUERY_OPTIONS)]
$config:data-root ! doc(. || "/" || $text)//db:title[ft:query(., $query, dbs:options())]
else
collection($config:data-root)//db:title[ft:query(., $query, $dbs:QUERY_OPTIONS)]
default return
if ($target-texts) then
for $text in $target-texts
return
$config:data-root ! doc(. || "/" || $text)//db:section[ft:query(., $query, $dbs:QUERY_OPTIONS)] |
$config:data-root ! doc(. || "/" || $text)//db:article[ft:query(., $query, $dbs:QUERY_OPTIONS)]
$config:data-root ! doc(. || "/" || $text)//db:section[ft:query(., $query, dbs:options())] |
$config:data-root ! doc(. || "/" || $text)//db:article[ft:query(., $query, dbs:options())]
else
collection($config:data-root)//db:section[ft:query(., $query, $dbs:QUERY_OPTIONS)] |
collection($config:data-root)//db:article[ft:query(., $query, $dbs:QUERY_OPTIONS)]
collection($config:data-root)//db:section[ft:query(., $query, dbs:options())] |
collection($config:data-root)//db:article[ft:query(., $query, dbs:options())]
else ()
};
declare function dbs:options() {
map:merge((
$dbs:QUERY_OPTIONS,
map {
"facets":
map:merge((
for $param in request:get-parameter-names()[starts-with(., 'facet-')]
let $dimension := substring-after($param, 'facet-')
return
map {
$dimension: request:get-parameter($param, ())
}
))
}
))
};
declare function dbs:autocomplete($doc as xs:string?, $fields as xs:string+, $q as xs:string) {
for $field in $fields
return
......
xquery version "3.1";
declare namespace tei="http://www.tei-c.org/ns/1.0";
import module namespace config="http://www.tei-c.org/tei-simple/config" at "config.xqm";
declare function local:sort($facets as map(*)?) {
array {
if (exists($facets)) then
for $key in map:keys($facets)
let $value := map:get($facets, $key)
order by $key ascending
return
map { $key: $value }
else
()
}
};
declare function local:print-table($config as map(*), $nodes as element()+, $values as xs:string*, $params as xs:string*) {
let $all := exists($config?max) and request:get-parameter("all-" || $config?dimension, ())
let $count := if ($all) then () else $config?max
let $facets :=
if (exists($values)) then
ft:facets($nodes, $config?dimension, $count, $values)
else
ft:facets($nodes, $config?dimension, $count)
return
if (map:size($facets) > 0) then
<table>
{
array:for-each(local:sort($facets), function($entry) {
map:for-each($entry, function($label, $freq) {
<tr>
<td>
<paper-checkbox class="facet" name="facet-{$config?dimension}" value="{$label}">
{ if ($label = $params[1]) then attribute checked { "checked" } else () }
{
if (exists($config?output)) then
$config?output($label)
else
$label
}
</paper-checkbox>
</td>
<td>{$freq}</td>
</tr>
})
}),
if (empty($params)) then
()
else
let $nested := local:print-table($config, $nodes, ($values, head($params)), tail($params))
return
if ($nested) then
<tr class="nested">
<td colspan="2">
{$nested}
</td>
</tr>
else
()
}
</table>
else
()
};
declare function local:display($config as map(*), $nodes as element()+) {
<div>
<h3>{$config?heading}
{
if (exists($config?max)) then
<paper-checkbox class="facet" name="all-{$config?dimension}">
{ if (request:get-parameter("all-" || $config?dimension, ())) then attribute checked { "checked" } else () }
Show all
</paper-checkbox>
else
()
}
</h3>
{
let $params := request:get-parameter("facet-" || $config?dimension, ())
return
local:print-table($config, $nodes, (), $params)
}
</div>
};
let $hits := session:get-attribute("apps.simple")
where count($hits) > 0
return
<div>
{
for $config in $config:facets?*
return
local:display($config, $hits)
}
</div>
......@@ -56,7 +56,7 @@ declare
function app:sort($items as element()*, $sortBy as xs:string?) {
let $items :=
if (count($config:data-exclude) = 1) then
$items[not(matches(util:collection-name(.), $config:data-exclude))]
$items[not(matches(document-uri(root(.)), $config:data-exclude))]
else
$items
return
......@@ -91,6 +91,7 @@ function app:list-works($node as node(), $model as map(*), $filter as xs:string?
let $sorted := app:sort($filtered, $sort)
return (
session:set-attribute('apps.simple', $filtered),
session:set-attribute('teipublisher.docs', $filtered),
session:set-attribute("teipublisher.works", $sorted),
session:set-attribute("teipublisher.browse", $browse),
session:set-attribute("teipublisher.filter", $filter),
......@@ -113,7 +114,9 @@ declare function app:options($sortBy as xs:string) {
$dimension: request:get-parameter($param, ())
}
)),
"fields": $sortBy
"fields": $sortBy,
"leading-wildcard": "yes",
"filter-rewrite": "yes"
}
};
......
......@@ -408,32 +408,6 @@ declare function pages:get-content($config as map(*), $div as element()) {
nav:get-content($config, $div)
};
declare function pages:breadcrumbs($node as node(), $model as map(*)) {
let $parent := ($model?data/self::tei:body, $model?data/ancestor-or-self::tei:div[1])[1]
let $parent-id := config:get-identifier($parent)
let $current-view:=
if($model?config?view != $config:default-view) then "&amp;view=" || $model?config?view else ()
let $current-odd:=
if($model?config?odd != $config:odd) then "&amp;odd=" || $model?config?odd else ()
return
<ol class="headings breadcrumb">
<li><a href="{$parent-id}">{nav:get-document-title($model?config, $model('data')/ancestor-or-self::tei:TEI)}</a></li>
{
for $parentDiv in $model?data/ancestor-or-self::tei:div[tei:head]
let $id := util:node-id(
if ($model?config?view = "page") then $parentDiv/preceding::tei:pb[1] else $parentDiv
)
return
<li>
<a href="{$parent-id}?root={$id}{$current-view}{$current-odd}">{$parentDiv/tei:head/string()}</a>
</li>
}
</ol>
};
declare
%templates:wrap
function pages:navigation-title($node as node(), $model as map(*)) {
......
......@@ -25,7 +25,7 @@ import module namespace config="http://www.tei-c.org/tei-simple/config" at "conf
declare function nav:get-root($root as xs:string?, $options as map(*)?) {
$config:data-root !
collection(. || "/" || $root)//dbk:section[ft:query(., (), $options)]/ancestor::dbk:article
collection(. || "/" || $root)//dbk:section[ft:query(., "file:*", $options)]/ancestor::dbk:article
};
declare function nav:get-header($config as map(*), $node as element()) {
......
......@@ -24,7 +24,7 @@ declare namespace tei="http://www.tei-c.org/ns/1.0";
import module namespace config="http://www.tei-c.org/tei-simple/config" at "config.xqm";
declare function nav:get-root($root as xs:string?, $options as map(*)?) {
$config:data-root ! collection(. || "/" || $root)//tei:body[ft:query(., (), $options)]/ancestor::tei:TEI
$config:data-root ! collection(. || "/" || $root)//tei:text[ft:query(., "file:*", $options)]/ancestor::tei:TEI
};
declare function nav:get-header($config as map(*), $node as element()) {
......@@ -94,7 +94,7 @@ declare function nav:get-first-page-start($config as map(*), $data as element())
if ($pb) then
$pb
else
$data/tei:TEI//tei:body
$data/tei:TEI//tei:text
};
......@@ -109,7 +109,7 @@ declare function nav:get-content($config as map(*), $div as element()) {
if ($nextPage) then
($div/ancestor::* intersect $nextPage/ancestor::*)[last()]
else
($div/ancestor::tei:div, $div/ancestor::tei:body)[1]
($div/ancestor::tei:div, $div/ancestor::tei:text)[1]
)
return
$chunk
......
......@@ -46,7 +46,7 @@ declare function teis:query-default($fields as xs:string+, $query as xs:string,
for $text in $target-texts
return
$config:data-root ! doc(. || "/" || $text)//tei:div[ft:query(., $query, teis:options())] |
$config:data-root ! doc(. || "/" || $text)//tei:body[ft:query(., $query, teis:options())]
$config:data-root ! doc(. || "/" || $text)//tei:text[ft:query(., $query, teis:options())]
else
collection($config:data-root)//tei:div[ft:query(., $query, teis:options())] |
collection($config:data-root)//tei:body[ft:query(., $query, teis:options())]
......@@ -73,7 +73,7 @@ declare function teis:options() {
declare function teis:query-metadata($field as xs:string, $query as xs:string, $sort as xs:string) {
let $options := map:merge((teis:options(), map { "fields": $sort }))
for $rootCol in $config:data-root
for $doc in collection($rootCol)//tei:body[ft:query(., $field || ":(" || $query || ")", $options)]
for $doc in collection($rootCol)//tei:text[ft:query(., $field || ":(" || $query || ")", $options)]
return
$doc/ancestor::tei:TEI
};
......@@ -107,7 +107,7 @@ declare function teis:autocomplete($doc as xs:string?, $fields as xs:string+, $q
function($key, $count) {
$key
}, 30, "lucene-index"),
collection($config:data-root)/util:index-keys-by-qname(xs:QName("tei:body"), $q,
collection($config:data-root)/util:index-keys-by-qname(xs:QName("tei:text"), $q,
function($key, $count) {
$key
}, 30, "lucene-index")
......@@ -169,12 +169,12 @@ declare function teis:expand($data as element()) {
if ($field = "text") then
(
($data/ancestor::tei:div intersect $nextPage/ancestor::tei:div)[last()],
$data/ancestor::tei:body
$data/ancestor::tei:text
)[1]
else
$data/ancestor::tei:body
$data/ancestor::tei:text
else
($data/ancestor::tei:div, $data/ancestor::tei:body)[1]
($data/ancestor::tei:div, $data/ancestor::tei:text)[1]
else
$data
let $expanded :=
......@@ -195,7 +195,7 @@ declare %private function teis:query-default-view($context as element()*, $query
$context[./descendant-or-self::tei:head[ft:query(., $query, $teis:QUERY_OPTIONS)]]
default return
$context[./descendant-or-self::tei:div[ft:query(., $query, $teis:QUERY_OPTIONS)]] |
$context[./descendant-or-self::tei:body[ft:query(., $query, $teis:QUERY_OPTIONS)]]
$context[./descendant-or-self::tei:text[ft:query(., $query, $teis:QUERY_OPTIONS)]]
};
declare function teis:get-current($config as map(*), $div as element()?) {
......
xquery version "1.0";
xquery version "3.1";
import module namespace xdb="http://exist-db.org/xquery/xmldb";
......@@ -29,4 +29,4 @@ declare function local:mkcol($collection, $path) {
(: store the collection configuration :)
local:mkcol("/db/system/config", $target),
xdb:store-files-from-pattern(concat("/system/config", $target), $dir, "**/*.xconf","text/xml",true())
\ No newline at end of file
xdb:store-files-from-pattern("/db/system/config/" || $target, $dir, "**/*.xconf","text/xml",true())
......@@ -108,6 +108,24 @@
.hi {
display: block;
}
#facets h3 {
display: flex;
flex-direction: row;
align-self: center;
justify-content: space-between;
}
#facets h3 paper-checkbox {
font-size: 85%;
}
#facets table td:nth-child(2) {
color: #808080;
text-align: right;
padding-left: 1em;
vertical-align: middle;
}
</style>
</custom-style>
</head>
......@@ -159,22 +177,23 @@
</pb-login>
</app-toolbar>
<app-toolbar>
<pb-paginate per-page="10" range="5" found-label="i18n(found)"></pb-paginate>
<pb-paginate per-page="10" range="5" found-label="i18n(found)" subscribe="results"></pb-paginate>
<pb-progress indeterminate="indeterminate" bottom-item="bottom-item"/>
</app-toolbar>
</app-header>
<main>
<pb-load id="results" url="templates/search-results.html"></pb-load>
<pb-load id="results" url="templates/search-results.html" emit="results"></pb-load>
<div class="search-panel">
<paper-card>
<div class="card-content">
<pb-search id="search-form" place-holder="i18n(search-query-attr)"
<pb-search id="search-form" place-holder="i18n(search-query-attr)" subscribe="search"
data-template="pages:parse-params" value="${query}" submit-on-load="submit-on-load">
<div class="targets">
<paper-checkbox name="field" value="text">Search sections</paper-checkbox>
<paper-checkbox name="field" value="head">Search headings</paper-checkbox>
</div>
<pb-custom-form id="facets" url="modules/facets.xql" subscribe="results" event="pb-results-received" emit="search"/>
</pb-search>
</div>
</paper-card>
......@@ -191,5 +210,26 @@
</div>
</paper-dialog>
</pb-page>
<script>
const facets = document.getElementById('facets');
console.log(facets);
facets.addEventListener('pb-custom-form-loaded', function(ev) {
const elems = ev.detail.querySelectorAll('.facet');
console.log(elems.length);
elems.forEach(facet => {
facet.addEventListener('change', () => {
const table = facet.closest('table');
if (table) {
const nested = table.querySelectorAll('.nested .facet').forEach(nested => {
if (nested != facet) {
nested.checked = false;
}
});
}
facets._submit();
});
});
});
</script>
</body>
</html>
......@@ -42,7 +42,7 @@ declare variable $config:default-view := "$$default-view$$";
declare variable $config:default-template := "view.html";
(:
: The element to search by default, either 'tei:div' or 'tei:body'.
: The element to search by default, either 'tei:div' or 'tei:text'.
:)
declare variable $config:search-default := "$$default-search$$";
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment