Skip to content
This repository was archived by the owner on Sep 19, 2022. It is now read-only.

Commit ebf4c20

Browse files
BaranekDvyskocilpavel
authored andcommitted
feat: WAYF search by localized name and domain
1 parent 528ec6e commit ebf4c20

File tree

4 files changed

+266
-82
lines changed

4 files changed

+266
-82
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
All notable changes to this project will be documented in this file.
33

44
## [Unreleased]
5+
#### Changed
6+
- Improve WAYF searching by localized name and domain
7+
58
#### Fixed
69
- Detailed endpoint format when spaced in EndpointMapToArray
710
- Revert change to INDEX_MIN in EndpointMapToArray

lib/Disco.php

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,9 +486,11 @@ public static function showEntry(
486486
array $metadata,
487487
$favourite = false
488488
): string {
489+
$searchData = htmlspecialchars(self::constructSearchData($metadata));
489490
$extra = ($favourite ? ' favourite' : '');
490491
$href = $t->getContinueUrl($metadata[self::IDP_ENTITY_ID]);
491-
$html = '<a class="metaentry' . $extra . ' list-group-item" href="' . $href. '">';
492+
$html = '<a class="metaentry' . $extra .
493+
' list-group-item" data-search="' . $searchData . '" href="' . $href. '">';
492494
$html .= '<strong>' . $t->getTranslatedEntityName($metadata) . '</strong>';
493495
$html .= '</a>';
494496

@@ -773,11 +775,54 @@ public static function getScripts(bool $boxed): string
773775
{
774776
$html = '<script type="text/javascript" src="' .
775777
Module::getModuleUrl('discopower/assets/js/suggest.js') . '"></script>' . PHP_EOL;
778+
779+
$html .= '<script type="text/javascript" src="' .
780+
Module::getModuleUrl('perun/res/js/jquery.livesearch.js') . '"></script>' . PHP_EOL;
781+
776782
$html .= '<script type="text/javascript" src="' .
777783
Module::getModuleUrl('perun/res/js/disco.js') . '"></script>' . PHP_EOL;
778784
if ($boxed) {
779785
$html .= Disco::boxedDesignScript() . PHP_EOL;
780786
}
781787
return $html;
782788
}
789+
790+
private static function arrayFlatten($array): array
791+
{
792+
$return = [];
793+
if (is_array($array)) {
794+
foreach ($array as $key => $value) {
795+
if (is_array($value)) {
796+
$return = array_merge($return, self::arrayFlatten($value));
797+
} else {
798+
$return[$key] = $value;
799+
}
800+
}
801+
} else {
802+
$return = [ $array ];
803+
}
804+
805+
return $return;
806+
}
807+
808+
private static function constructSearchData($idpMetadata): string
809+
{
810+
$res = '';
811+
if (!empty($idpMetadata['UIInfo'])) {
812+
$newEl = array_merge($idpMetadata['UIInfo']);
813+
unset($idpMetadata['UIInfo']);
814+
$idpMetadata = array_merge($idpMetadata, $newEl);
815+
}
816+
817+
$keys = ['entityid', 'OrganizationName', 'OrganizationDisplayName',
818+
'name', 'url', 'OrganizationURL', 'scope', 'DisplayName'];
819+
820+
foreach ($keys as $key) {
821+
if (!empty($idpMetadata[$key])) {
822+
$res .= (' ' . implode(' ', self::arrayFlatten($idpMetadata[$key])));
823+
}
824+
}
825+
826+
return strtolower(str_replace('"', '', $res));
827+
}
783828
}

www/res/js/disco.js

Lines changed: 45 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,57 @@
1-
let forceShow = false;
2-
3-
function setForceShow(val) {
4-
forceShow = val;
5-
}
6-
7-
$.fn.liveUpdate = function(list) {
8-
list = $(list);
9-
if (list.length) {
10-
var rows = list.children('a'), cache = rows.map(function () {
11-
return jQuery(this).text().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, "");
12-
});
13-
14-
this.keyup(filter).keyup().parents('form').submit(function () {
15-
return false;
16-
});
17-
}
18-
return this;
19-
20-
function filter() {
21-
const searchTerm = $.trim($(this).val().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ""));
22-
const scores = [];
23-
rows.hide();
24-
showNoEntriesFound(false);
25-
if (!searchTerm) {
26-
setForceShow(false);
27-
return;
28-
}
29-
cache.each(function (i) {
30-
const score = this.score(searchTerm);
31-
if (score > 0) {
32-
scores.push([score, i]);
33-
}
34-
});
35-
if (scores.length === 0) {
36-
showNoEntriesFound(true);
37-
} else if (!forceShow && scores.length > 7) {
38-
showWarningTooMuchEntries(true, scores.length + 1);
39-
} else {
40-
$.each(
41-
scores.sort(
42-
function(a, b) { return b[0] - a[0];}),
43-
function() {$(rows[this[1]]).show();
44-
});
45-
}
46-
}
47-
};
48-
49-
function showWarningTooMuchEntries(show, cnt = 0) {
1+
function hideAll () {
2+
$("#list").hide();
503
$('#no-entries').hide();
51-
if (show) {
52-
$('#warning-entries').show();
53-
} else {
54-
$('#warning-entries').hide();
55-
}
56-
$('#results-cnt').text(cnt);
57-
}
58-
59-
function showNoEntriesFound(show) {
604
$('#warning-entries').hide();
61-
if (show) {
62-
$('#no-entries').show();
63-
} else {
64-
$('#no-entries').hide();
65-
}
665
}
676

687
$(document).ready(function() {
69-
$("#query").liveUpdate("#list");
70-
$("#showEntries").click(function() {
71-
$("#last-used-idp-wrap").hide();
72-
$("#entries").show();
73-
$("#showEntries").hide();
74-
});
75-
$("#showEntriesFromDropdown").click(function() {
76-
$("#dropdown-entries").toggle();
77-
});
788
if ($("#last-used-idp-wrap").length > 0) {
799
$("#last-used-idp .metaentry").focus();
8010
} else {
8111
$("#entries").show();
8212
}
83-
$('#warning-entries-btn-force-show').click(function() {
84-
$('#query').trigger('keyup');
85-
showWarningTooMuchEntries(false);
86-
showEntries();
13+
14+
let forceShow = false
15+
$('#query').keyup(function() {
16+
const filter = $(this).val().trim().toLowerCase();
17+
if (!filter) {
18+
hideAll();
19+
forceShow = false;
20+
} else {
21+
let matches = [];
22+
$('#list a').each(function () {
23+
if ($(this).attr('data-search').indexOf(filter) >= 0) {
24+
matches.push(this);
25+
} else {
26+
$(this).hide();
27+
}
28+
});
29+
if (matches.length <= 0) {
30+
$('#no-entries').show();
31+
$('#warning-entries').hide();
32+
} else {
33+
$("#list").show();
34+
$('#results-cnt').text(matches.length);
35+
$('#no-entries').hide();
36+
if (matches.length > 10 && !forceShow) {
37+
matches.forEach(m => {
38+
$(m).hide();
39+
});
40+
$('#warning-entries').show();
41+
} else {
42+
$('#warning-entries').hide();
43+
matches.forEach(m => {
44+
$(m).show();
45+
});
46+
}
47+
}
48+
}
8749
});
88-
});
8950

90-
function showEntries() {
91-
setForceShow(true);
92-
$('#query').trigger('keyup');
93-
}
51+
$('#warning-entries-btn-force-show').click(function(event) {
52+
event.preventDefault();
53+
forceShow = true;
54+
$('#warning-entries').hide();
55+
$('#query').trigger('keyup').focus();
56+
});
57+
});

www/res/js/jquery.livesearch.js

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
(function ($) {
2+
3+
//The timeout has to live in this scope
4+
var timeout;
5+
6+
function LiveSearch($elem, options) {
7+
this.$elem = $elem;
8+
this.$form = $elem.closest('form');
9+
this.options = $.extend({
10+
delay: 400,
11+
minimum_characters: 3,
12+
serialize: this.$form,
13+
client_side_cache: true,
14+
process_data: false,
15+
dataType: 'json'
16+
}, options);
17+
this.url = this.options.url || this.$form.attr('action');
18+
19+
if (this.options.file_extension) {
20+
this.ajax_url = this.ensure_file_extension(this.options.file_extension);
21+
} else {
22+
this.ajax_url = this.url;
23+
}
24+
25+
this.last_search = false;
26+
this.search_xhr = false;
27+
if (this.options.client_side_cache) {
28+
this.cache = {};
29+
} else {
30+
this.cache = false;
31+
}
32+
this.active = true;
33+
this._attach();
34+
}
35+
36+
$.fn.livesearch = function (options) {
37+
options = options || {};
38+
return $(this).each(function () {
39+
var livesearch = $(this).data('livesearch');
40+
if (!livesearch) {
41+
livesearch = new LiveSearch($(this), options);
42+
$(this).data('livesearch', livesearch);
43+
}
44+
});
45+
};
46+
47+
$.extend(LiveSearch.prototype, {
48+
_attach: function () {
49+
var _this = this;
50+
51+
this.$elem.attr('autocomplete', 'off'); //we got this, yall
52+
53+
this.$elem.on('keypress cut paste input', function () {
54+
if (!_this.active) {
55+
return;
56+
}
57+
58+
clearTimeout(timeout);
59+
timeout = setTimeout(function () {
60+
_this.search();
61+
}, _this.options.delay);
62+
});
63+
64+
// Often, process_data is used to create the entire param set, so we
65+
// can't trust options.serialize to indicate the interesting form fields
66+
// for on change events.
67+
if (!this.options.process_data) {
68+
this.options.serialize.on('change', function () {
69+
_this.search();
70+
});
71+
}
72+
73+
this.$elem.on('livesearch:suspend', function () {
74+
_this.active = false;
75+
});
76+
this.$elem.on('livesearch:activate', function () {
77+
_this.active = true;
78+
});
79+
this.$elem.on('livesearch:cancel', function () {
80+
if (_this.search_xhr) {
81+
_this.search_xhr.abort();
82+
}
83+
_this.last_search = false;
84+
});
85+
},
86+
87+
ensure_file_extension: function (extension) {
88+
var
89+
host_regexp_string = window.location.host.replace(/[^\w\d]/g, function (m) {
90+
return '\\' + m;
91+
}),
92+
file_extension_regexp = new RegExp('((?:' + host_regexp_string + ')?[^\\.$#\\?]+)(\\.\\w*|)($|#|\\?)');
93+
94+
return this.url.replace(file_extension_regexp, function (m, _1, _2, _3) {
95+
return _1 + '.' + extension + _3;
96+
});
97+
},
98+
99+
suspend_while: function (func) {
100+
this.active = false;
101+
func();
102+
// TODO: this timeout is to to allow events to bubble before re-enabling,
103+
// but I'm not sure why bubbling doesn't occur synchronously.
104+
var _this = this;
105+
setTimeout(function () {
106+
_this.active = true;
107+
}, 100);
108+
},
109+
110+
search: function () {
111+
var _this = this,
112+
form_data = this.options.serialize.serialize();
113+
114+
115+
if (this.options.process_data) {
116+
form_data = this.options.process_data.apply(this, [form_data]);
117+
if (typeof form_data === 'object') {
118+
form_data = $.param(form_data);
119+
}
120+
}
121+
122+
if (form_data === this.last_search) {
123+
return;
124+
}
125+
if (this.$elem.val().length < this.options.minimum_characters) {
126+
return;
127+
}
128+
129+
if (this.search_xhr) {
130+
this.search_xhr.abort();
131+
}
132+
133+
if (this.cache && this.cache[form_data] && typeof (this.cache[form_data]) !== 'function') {
134+
this.$elem.trigger('livesearch:results', [this.cache[form_data]]);
135+
} else {
136+
this.$elem.trigger('livesearch:searching');
137+
this.$elem.addClass('searching');
138+
139+
this.search_xhr = $.ajax({
140+
type: 'get',
141+
url: this.ajax_url,
142+
dataType: this.options.dataType,
143+
data: form_data,
144+
global: false,
145+
success: function (data) {
146+
// this is the best workaround I can think of for
147+
// http://dev.jquery.com/ticket/6173
148+
if (data === null) {
149+
return;
150+
}
151+
152+
_this.$elem.trigger('livesearch:results', [data]);
153+
_this.$elem.removeClass('searching');
154+
if (_this.cache) {
155+
_this.cache[form_data] = data;
156+
}
157+
},
158+
error: function () {
159+
_this.$elem.trigger('livesearch:ajax_error');
160+
_this.$elem.removeClass('searching');
161+
},
162+
statusCode: {
163+
304: function () {
164+
}
165+
}
166+
});
167+
}
168+
169+
this.last_search = form_data;
170+
}
171+
});
172+
});

0 commit comments

Comments
 (0)