2023-10-03 17:55:41 +02:00
/*global SelectBox, gettext, interpolate, quickElement, SelectFilter*/
/ *
SelectFilter2 - Turns a multiple - select box into a filter interface .
Requires core . js and SelectBox . js .
* /
'use strict' ;
{
window . SelectFilter = {
init : function ( field _id , field _name , is _stacked ) {
if ( field _id . match ( /__prefix__/ ) ) {
// Don't initialize on empty forms.
return ;
}
const from _box = document . getElementById ( field _id ) ;
from _box . id += '_from' ; // change its ID
from _box . className = 'filtered' ;
for ( const p of from _box . parentNode . getElementsByTagName ( 'p' ) ) {
if ( p . classList . contains ( "info" ) ) {
// Remove <p class="info">, because it just gets in the way.
from _box . parentNode . removeChild ( p ) ;
} else if ( p . classList . contains ( "help" ) ) {
// Move help text up to the top so it isn't below the select
// boxes or wrapped off on the side to the right of the add
// button:
from _box . parentNode . insertBefore ( p , from _box . parentNode . firstChild ) ;
}
}
// <div class="selector"> or <div class="selector stacked">
const selector _div = quickElement ( 'div' , from _box . parentNode ) ;
2024-03-12 22:19:25 +01:00
// Make sure the selector div is at the beginning so that the
// add link would be displayed to the right of the widget.
from _box . parentNode . prepend ( selector _div ) ;
2023-10-03 17:55:41 +02:00
selector _div . className = is _stacked ? 'selector stacked' : 'selector' ;
// <div class="selector-available">
const selector _available = quickElement ( 'div' , selector _div ) ;
selector _available . className = 'selector-available' ;
const title _available = quickElement ( 'h2' , selector _available , interpolate ( gettext ( 'Available %s' ) + ' ' , [ field _name ] ) ) ;
quickElement (
'span' , title _available , '' ,
'class' , 'help help-tooltip help-icon' ,
'title' , interpolate (
gettext (
'This is the list of available %s. You may choose some by ' +
'selecting them in the box below and then clicking the ' +
'"Choose" arrow between the two boxes.'
) ,
[ field _name ]
)
) ;
const filter _p = quickElement ( 'p' , selector _available , '' , 'id' , field _id + '_filter' ) ;
filter _p . className = 'selector-filter' ;
const search _filter _label = quickElement ( 'label' , filter _p , '' , 'for' , field _id + '_input' ) ;
quickElement (
'span' , search _filter _label , '' ,
'class' , 'help-tooltip search-label-icon' ,
'title' , interpolate ( gettext ( "Type into this box to filter down the list of available %s." ) , [ field _name ] )
) ;
filter _p . appendChild ( document . createTextNode ( ' ' ) ) ;
const filter _input = quickElement ( 'input' , filter _p , '' , 'type' , 'text' , 'placeholder' , gettext ( "Filter" ) ) ;
filter _input . id = field _id + '_input' ;
selector _available . appendChild ( from _box ) ;
const choose _all = quickElement ( 'a' , selector _available , gettext ( 'Choose all' ) , 'title' , interpolate ( gettext ( 'Click to choose all %s at once.' ) , [ field _name ] ) , 'href' , '#' , 'id' , field _id + '_add_all_link' ) ;
choose _all . className = 'selector-chooseall' ;
// <ul class="selector-chooser">
const selector _chooser = quickElement ( 'ul' , selector _div ) ;
selector _chooser . className = 'selector-chooser' ;
const add _link = quickElement ( 'a' , quickElement ( 'li' , selector _chooser ) , gettext ( 'Choose' ) , 'title' , gettext ( 'Choose' ) , 'href' , '#' , 'id' , field _id + '_add_link' ) ;
add _link . className = 'selector-add' ;
const remove _link = quickElement ( 'a' , quickElement ( 'li' , selector _chooser ) , gettext ( 'Remove' ) , 'title' , gettext ( 'Remove' ) , 'href' , '#' , 'id' , field _id + '_remove_link' ) ;
remove _link . className = 'selector-remove' ;
// <div class="selector-chosen">
const selector _chosen = quickElement ( 'div' , selector _div , '' , 'id' , field _id + '_selector_chosen' ) ;
selector _chosen . className = 'selector-chosen' ;
const title _chosen = quickElement ( 'h2' , selector _chosen , interpolate ( gettext ( 'Chosen %s' ) + ' ' , [ field _name ] ) ) ;
quickElement (
'span' , title _chosen , '' ,
'class' , 'help help-tooltip help-icon' ,
'title' , interpolate (
gettext (
'This is the list of chosen %s. You may remove some by ' +
'selecting them in the box below and then clicking the ' +
'"Remove" arrow between the two boxes.'
) ,
[ field _name ]
)
) ;
const filter _selected _p = quickElement ( 'p' , selector _chosen , '' , 'id' , field _id + '_filter_selected' ) ;
filter _selected _p . className = 'selector-filter' ;
const search _filter _selected _label = quickElement ( 'label' , filter _selected _p , '' , 'for' , field _id + '_selected_input' ) ;
quickElement (
'span' , search _filter _selected _label , '' ,
'class' , 'help-tooltip search-label-icon' ,
'title' , interpolate ( gettext ( "Type into this box to filter down the list of selected %s." ) , [ field _name ] )
) ;
filter _selected _p . appendChild ( document . createTextNode ( ' ' ) ) ;
const filter _selected _input = quickElement ( 'input' , filter _selected _p , '' , 'type' , 'text' , 'placeholder' , gettext ( "Filter" ) ) ;
filter _selected _input . id = field _id + '_selected_input' ;
const to _box = quickElement ( 'select' , selector _chosen , '' , 'id' , field _id + '_to' , 'multiple' , '' , 'size' , from _box . size , 'name' , from _box . name ) ;
to _box . className = 'filtered' ;
const warning _footer = quickElement ( 'div' , selector _chosen , '' , 'class' , 'list-footer-display' ) ;
quickElement ( 'span' , warning _footer , '' , 'id' , field _id + '_list-footer-display-text' ) ;
quickElement ( 'span' , warning _footer , ' (click to clear)' , 'class' , 'list-footer-display__clear' ) ;
const clear _all = quickElement ( 'a' , selector _chosen , gettext ( 'Remove all' ) , 'title' , interpolate ( gettext ( 'Click to remove all chosen %s at once.' ) , [ field _name ] ) , 'href' , '#' , 'id' , field _id + '_remove_all_link' ) ;
clear _all . className = 'selector-clearall' ;
from _box . name = from _box . name + '_old' ;
// Set up the JavaScript event handlers for the select box filter interface
const move _selection = function ( e , elem , move _func , from , to ) {
if ( elem . classList . contains ( 'active' ) ) {
move _func ( from , to ) ;
SelectFilter . refresh _icons ( field _id ) ;
SelectFilter . refresh _filtered _selects ( field _id ) ;
SelectFilter . refresh _filtered _warning ( field _id ) ;
}
e . preventDefault ( ) ;
} ;
choose _all . addEventListener ( 'click' , function ( e ) {
move _selection ( e , this , SelectBox . move _all , field _id + '_from' , field _id + '_to' ) ;
} ) ;
add _link . addEventListener ( 'click' , function ( e ) {
move _selection ( e , this , SelectBox . move , field _id + '_from' , field _id + '_to' ) ;
} ) ;
remove _link . addEventListener ( 'click' , function ( e ) {
move _selection ( e , this , SelectBox . move , field _id + '_to' , field _id + '_from' ) ;
} ) ;
clear _all . addEventListener ( 'click' , function ( e ) {
move _selection ( e , this , SelectBox . move _all , field _id + '_to' , field _id + '_from' ) ;
} ) ;
warning _footer . addEventListener ( 'click' , function ( e ) {
filter _selected _input . value = '' ;
SelectBox . filter ( field _id + '_to' , '' ) ;
SelectFilter . refresh _filtered _warning ( field _id ) ;
SelectFilter . refresh _icons ( field _id ) ;
} ) ;
filter _input . addEventListener ( 'keypress' , function ( e ) {
SelectFilter . filter _key _press ( e , field _id , '_from' , '_to' ) ;
} ) ;
filter _input . addEventListener ( 'keyup' , function ( e ) {
SelectFilter . filter _key _up ( e , field _id , '_from' ) ;
} ) ;
filter _input . addEventListener ( 'keydown' , function ( e ) {
SelectFilter . filter _key _down ( e , field _id , '_from' , '_to' ) ;
} ) ;
filter _selected _input . addEventListener ( 'keypress' , function ( e ) {
SelectFilter . filter _key _press ( e , field _id , '_to' , '_from' ) ;
} ) ;
filter _selected _input . addEventListener ( 'keyup' , function ( e ) {
SelectFilter . filter _key _up ( e , field _id , '_to' , '_selected_input' ) ;
} ) ;
filter _selected _input . addEventListener ( 'keydown' , function ( e ) {
SelectFilter . filter _key _down ( e , field _id , '_to' , '_from' ) ;
} ) ;
selector _div . addEventListener ( 'change' , function ( e ) {
if ( e . target . tagName === 'SELECT' ) {
SelectFilter . refresh _icons ( field _id ) ;
}
} ) ;
selector _div . addEventListener ( 'dblclick' , function ( e ) {
if ( e . target . tagName === 'OPTION' ) {
if ( e . target . closest ( 'select' ) . id === field _id + '_to' ) {
SelectBox . move ( field _id + '_to' , field _id + '_from' ) ;
} else {
SelectBox . move ( field _id + '_from' , field _id + '_to' ) ;
}
SelectFilter . refresh _icons ( field _id ) ;
}
} ) ;
from _box . closest ( 'form' ) . addEventListener ( 'submit' , function ( ) {
SelectBox . filter ( field _id + '_to' , '' ) ;
SelectBox . select _all ( field _id + '_to' ) ;
} ) ;
SelectBox . init ( field _id + '_from' ) ;
SelectBox . init ( field _id + '_to' ) ;
// Move selected from_box options to to_box
SelectBox . move ( field _id + '_from' , field _id + '_to' ) ;
// Initial icon refresh
SelectFilter . refresh _icons ( field _id ) ;
} ,
any _selected : function ( field ) {
// Temporarily add the required attribute and check validity.
field . required = true ;
const any _selected = field . checkValidity ( ) ;
field . required = false ;
return any _selected ;
} ,
refresh _filtered _warning : function ( field _id ) {
const count = SelectBox . get _hidden _node _count ( field _id + '_to' ) ;
const selector = document . getElementById ( field _id + '_selector_chosen' ) ;
const warning = document . getElementById ( field _id + '_list-footer-display-text' ) ;
selector . className = selector . className . replace ( 'selector-chosen--with-filtered' , '' ) ;
warning . textContent = interpolate ( ngettext (
'%s selected option not visible' ,
'%s selected options not visible' ,
count
) , [ count ] ) ;
if ( count > 0 ) {
selector . className += ' selector-chosen--with-filtered' ;
}
} ,
refresh _filtered _selects : function ( field _id ) {
SelectBox . filter ( field _id + '_from' , document . getElementById ( field _id + "_input" ) . value ) ;
SelectBox . filter ( field _id + '_to' , document . getElementById ( field _id + "_selected_input" ) . value ) ;
} ,
refresh _icons : function ( field _id ) {
const from = document . getElementById ( field _id + '_from' ) ;
const to = document . getElementById ( field _id + '_to' ) ;
// Active if at least one item is selected
document . getElementById ( field _id + '_add_link' ) . classList . toggle ( 'active' , SelectFilter . any _selected ( from ) ) ;
document . getElementById ( field _id + '_remove_link' ) . classList . toggle ( 'active' , SelectFilter . any _selected ( to ) ) ;
// Active if the corresponding box isn't empty
document . getElementById ( field _id + '_add_all_link' ) . classList . toggle ( 'active' , from . querySelector ( 'option' ) ) ;
document . getElementById ( field _id + '_remove_all_link' ) . classList . toggle ( 'active' , to . querySelector ( 'option' ) ) ;
SelectFilter . refresh _filtered _warning ( field _id ) ;
} ,
filter _key _press : function ( event , field _id , source , target ) {
const source _box = document . getElementById ( field _id + source ) ;
// don't submit form if user pressed Enter
if ( ( event . which && event . which === 13 ) || ( event . keyCode && event . keyCode === 13 ) ) {
source _box . selectedIndex = 0 ;
SelectBox . move ( field _id + source , field _id + target ) ;
source _box . selectedIndex = 0 ;
event . preventDefault ( ) ;
}
} ,
filter _key _up : function ( event , field _id , source , filter _input ) {
const input = filter _input || '_input' ;
const source _box = document . getElementById ( field _id + source ) ;
const temp = source _box . selectedIndex ;
SelectBox . filter ( field _id + source , document . getElementById ( field _id + input ) . value ) ;
source _box . selectedIndex = temp ;
SelectFilter . refresh _filtered _warning ( field _id ) ;
SelectFilter . refresh _icons ( field _id ) ;
} ,
filter _key _down : function ( event , field _id , source , target ) {
const source _box = document . getElementById ( field _id + source ) ;
// right key (39) or left key (37)
const direction = source === '_from' ? 39 : 37 ;
// right arrow -- move across
if ( ( event . which && event . which === direction ) || ( event . keyCode && event . keyCode === direction ) ) {
const old _index = source _box . selectedIndex ;
SelectBox . move ( field _id + source , field _id + target ) ;
SelectFilter . refresh _filtered _selects ( field _id ) ;
SelectFilter . refresh _filtered _warning ( field _id ) ;
source _box . selectedIndex = ( old _index === source _box . length ) ? source _box . length - 1 : old _index ;
return ;
}
// down arrow -- wrap around
if ( ( event . which && event . which === 40 ) || ( event . keyCode && event . keyCode === 40 ) ) {
source _box . selectedIndex = ( source _box . length === source _box . selectedIndex + 1 ) ? 0 : source _box . selectedIndex + 1 ;
}
// up arrow -- wrap around
if ( ( event . which && event . which === 38 ) || ( event . keyCode && event . keyCode === 38 ) ) {
source _box . selectedIndex = ( source _box . selectedIndex === 0 ) ? source _box . length - 1 : source _box . selectedIndex - 1 ;
}
}
} ;
window . addEventListener ( 'load' , function ( e ) {
document . querySelectorAll ( 'select.selectfilter, select.selectfilterstacked' ) . forEach ( function ( el ) {
const data = el . dataset ;
SelectFilter . init ( el . id , data . fieldName , parseInt ( data . isStacked , 10 ) ) ;
} ) ;
} ) ;
}