3061 <xsl:text>}(); // eslint-disable-line |
3059 <xsl:text>}(); // eslint-disable-line |
3062 </xsl:text> |
3060 </xsl:text> |
3063 <xsl:text> |
3061 <xsl:text> |
3064 </xsl:text> |
3062 </xsl:text> |
3065 </xsl:template> |
3063 </xsl:template> |
|
3064 <xsl:template mode="widget_class" match="widget[@type='DropDown']"> |
|
3065 <xsl:text> class DropDownWidget extends Widget{ |
|
3066 </xsl:text> |
|
3067 <xsl:text> dispatch(value) { |
|
3068 </xsl:text> |
|
3069 <xsl:text> if(!this.opened) this.set_selection(value); |
|
3070 </xsl:text> |
|
3071 <xsl:text> } |
|
3072 </xsl:text> |
|
3073 <xsl:text> init() { |
|
3074 </xsl:text> |
|
3075 <xsl:text> this.button_elt.onclick = this.on_button_click.bind(this); |
|
3076 </xsl:text> |
|
3077 <xsl:text> // Save original size of rectangle |
|
3078 </xsl:text> |
|
3079 <xsl:text> this.box_bbox = this.box_elt.getBBox() |
|
3080 </xsl:text> |
|
3081 <xsl:text> |
|
3082 </xsl:text> |
|
3083 <xsl:text> // Compute margins |
|
3084 </xsl:text> |
|
3085 <xsl:text> let text_bbox = this.text_elt.getBBox(); |
|
3086 </xsl:text> |
|
3087 <xsl:text> let lmargin = text_bbox.x - this.box_bbox.x; |
|
3088 </xsl:text> |
|
3089 <xsl:text> let tmargin = text_bbox.y - this.box_bbox.y; |
|
3090 </xsl:text> |
|
3091 <xsl:text> this.margins = [lmargin, tmargin].map(x => Math.max(x,0)); |
|
3092 </xsl:text> |
|
3093 <xsl:text> |
|
3094 </xsl:text> |
|
3095 <xsl:text> // Index of first visible element in the menu, when opened |
|
3096 </xsl:text> |
|
3097 <xsl:text> this.menu_offset = 0; |
|
3098 </xsl:text> |
|
3099 <xsl:text> |
|
3100 </xsl:text> |
|
3101 <xsl:text> // How mutch to lift the menu vertically so that it does not cross bottom border |
|
3102 </xsl:text> |
|
3103 <xsl:text> this.lift = 0; |
|
3104 </xsl:text> |
|
3105 <xsl:text> |
|
3106 </xsl:text> |
|
3107 <xsl:text> // Event handlers cannot be object method ('this' is unknown) |
|
3108 </xsl:text> |
|
3109 <xsl:text> // as a workaround, handler given to addEventListener is bound in advance. |
|
3110 </xsl:text> |
|
3111 <xsl:text> this.bound_close_on_click_elsewhere = this.close_on_click_elsewhere.bind(this); |
|
3112 </xsl:text> |
|
3113 <xsl:text> this.bound_on_selection_click = this.on_selection_click.bind(this); |
|
3114 </xsl:text> |
|
3115 <xsl:text> this.bound_on_backward_click = this.on_backward_click.bind(this); |
|
3116 </xsl:text> |
|
3117 <xsl:text> this.bound_on_forward_click = this.on_forward_click.bind(this); |
|
3118 </xsl:text> |
|
3119 <xsl:text> this.opened = false; |
|
3120 </xsl:text> |
|
3121 <xsl:text> } |
|
3122 </xsl:text> |
|
3123 <xsl:text> on_button_click() { |
|
3124 </xsl:text> |
|
3125 <xsl:text> this.open(); |
|
3126 </xsl:text> |
|
3127 <xsl:text> } |
|
3128 </xsl:text> |
|
3129 <xsl:text> // Called when a menu entry is clicked |
|
3130 </xsl:text> |
|
3131 <xsl:text> on_selection_click(selection) { |
|
3132 </xsl:text> |
|
3133 <xsl:text> this.close(); |
|
3134 </xsl:text> |
|
3135 <xsl:text> this.apply_hmi_value(0, selection); |
|
3136 </xsl:text> |
|
3137 <xsl:text> } |
|
3138 </xsl:text> |
|
3139 <xsl:text> on_backward_click(){ |
|
3140 </xsl:text> |
|
3141 <xsl:text> this.scroll(false); |
|
3142 </xsl:text> |
|
3143 <xsl:text> } |
|
3144 </xsl:text> |
|
3145 <xsl:text> on_forward_click(){ |
|
3146 </xsl:text> |
|
3147 <xsl:text> this.scroll(true); |
|
3148 </xsl:text> |
|
3149 <xsl:text> } |
|
3150 </xsl:text> |
|
3151 <xsl:text> set_selection(value) { |
|
3152 </xsl:text> |
|
3153 <xsl:text> let display_str; |
|
3154 </xsl:text> |
|
3155 <xsl:text> if(value >= 0 && value < this.content.length){ |
|
3156 </xsl:text> |
|
3157 <xsl:text> // if valid selection resolve content |
|
3158 </xsl:text> |
|
3159 <xsl:text> display_str = this.content[value]; |
|
3160 </xsl:text> |
|
3161 <xsl:text> this.last_selection = value; |
|
3162 </xsl:text> |
|
3163 <xsl:text> } else { |
|
3164 </xsl:text> |
|
3165 <xsl:text> // otherwise show problem |
|
3166 </xsl:text> |
|
3167 <xsl:text> display_str = "?"+String(value)+"?"; |
|
3168 </xsl:text> |
|
3169 <xsl:text> } |
|
3170 </xsl:text> |
|
3171 <xsl:text> // It is assumed that first span always stays, |
|
3172 </xsl:text> |
|
3173 <xsl:text> // and contains selection when menu is closed |
|
3174 </xsl:text> |
|
3175 <xsl:text> this.text_elt.firstElementChild.textContent = display_str; |
|
3176 </xsl:text> |
|
3177 <xsl:text> } |
|
3178 </xsl:text> |
|
3179 <xsl:text> grow_text(up_to) { |
|
3180 </xsl:text> |
|
3181 <xsl:text> let count = 1; |
|
3182 </xsl:text> |
|
3183 <xsl:text> let txt = this.text_elt; |
|
3184 </xsl:text> |
|
3185 <xsl:text> let first = txt.firstElementChild; |
|
3186 </xsl:text> |
|
3187 <xsl:text> // Real world (pixels) boundaries of current page |
|
3188 </xsl:text> |
|
3189 <xsl:text> let bounds = svg_root.getBoundingClientRect(); |
|
3190 </xsl:text> |
|
3191 <xsl:text> this.lift = 0; |
|
3192 </xsl:text> |
|
3193 <xsl:text> while(count < up_to) { |
|
3194 </xsl:text> |
|
3195 <xsl:text> let next = first.cloneNode(); |
|
3196 </xsl:text> |
|
3197 <xsl:text> // relative line by line text flow instead of absolute y coordinate |
|
3198 </xsl:text> |
|
3199 <xsl:text> next.removeAttribute("y"); |
|
3200 </xsl:text> |
|
3201 <xsl:text> next.setAttribute("dy", "1.1em"); |
|
3202 </xsl:text> |
|
3203 <xsl:text> // default content to allow computing text element bbox |
|
3204 </xsl:text> |
|
3205 <xsl:text> next.textContent = "..."; |
|
3206 </xsl:text> |
|
3207 <xsl:text> // append new span to text element |
|
3208 </xsl:text> |
|
3209 <xsl:text> txt.appendChild(next); |
|
3210 </xsl:text> |
|
3211 <xsl:text> // now check if text extended by one row fits to page |
|
3212 </xsl:text> |
|
3213 <xsl:text> // FIXME : exclude margins to be more accurate on box size |
|
3214 </xsl:text> |
|
3215 <xsl:text> let rect = txt.getBoundingClientRect(); |
|
3216 </xsl:text> |
|
3217 <xsl:text> if(rect.bottom > bounds.bottom){ |
|
3218 </xsl:text> |
|
3219 <xsl:text> // in case of overflow at the bottom, lift up one row |
|
3220 </xsl:text> |
|
3221 <xsl:text> let backup = first.getAttribute("dy"); |
|
3222 </xsl:text> |
|
3223 <xsl:text> // apply lift asr a dy added too first span (y attrib stays) |
|
3224 </xsl:text> |
|
3225 <xsl:text> first.setAttribute("dy", "-"+String((this.lift+1)*1.1)+"em"); |
|
3226 </xsl:text> |
|
3227 <xsl:text> rect = txt.getBoundingClientRect(); |
|
3228 </xsl:text> |
|
3229 <xsl:text> if(rect.top > bounds.top){ |
|
3230 </xsl:text> |
|
3231 <xsl:text> this.lift += 1; |
|
3232 </xsl:text> |
|
3233 <xsl:text> } else { |
|
3234 </xsl:text> |
|
3235 <xsl:text> // if it goes over the top, then backtrack |
|
3236 </xsl:text> |
|
3237 <xsl:text> // restore dy attribute on first span |
|
3238 </xsl:text> |
|
3239 <xsl:text> if(backup) |
|
3240 </xsl:text> |
|
3241 <xsl:text> first.setAttribute("dy", backup); |
|
3242 </xsl:text> |
|
3243 <xsl:text> else |
|
3244 </xsl:text> |
|
3245 <xsl:text> first.removeAttribute("dy"); |
|
3246 </xsl:text> |
|
3247 <xsl:text> // remove unwanted child |
|
3248 </xsl:text> |
|
3249 <xsl:text> txt.removeChild(next); |
|
3250 </xsl:text> |
|
3251 <xsl:text> return count; |
|
3252 </xsl:text> |
|
3253 <xsl:text> } |
|
3254 </xsl:text> |
|
3255 <xsl:text> } |
|
3256 </xsl:text> |
|
3257 <xsl:text> count++; |
|
3258 </xsl:text> |
|
3259 <xsl:text> } |
|
3260 </xsl:text> |
|
3261 <xsl:text> return count; |
|
3262 </xsl:text> |
|
3263 <xsl:text> } |
|
3264 </xsl:text> |
|
3265 <xsl:text> close_on_click_elsewhere(e) { |
|
3266 </xsl:text> |
|
3267 <xsl:text> // inhibit events not targetting spans (menu items) |
|
3268 </xsl:text> |
|
3269 <xsl:text> if(e.target.parentNode !== this.text_elt){ |
|
3270 </xsl:text> |
|
3271 <xsl:text> e.stopPropagation(); |
|
3272 </xsl:text> |
|
3273 <xsl:text> // close menu in case click is outside box |
|
3274 </xsl:text> |
|
3275 <xsl:text> if(e.target !== this.box_elt) |
|
3276 </xsl:text> |
|
3277 <xsl:text> this.close(); |
|
3278 </xsl:text> |
|
3279 <xsl:text> } |
|
3280 </xsl:text> |
|
3281 <xsl:text> } |
|
3282 </xsl:text> |
|
3283 <xsl:text> close(){ |
|
3284 </xsl:text> |
|
3285 <xsl:text> // Stop hogging all click events |
|
3286 </xsl:text> |
|
3287 <xsl:text> svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true); |
|
3288 </xsl:text> |
|
3289 <xsl:text> // Restore position and sixe of widget elements |
|
3290 </xsl:text> |
|
3291 <xsl:text> this.reset_text(); |
|
3292 </xsl:text> |
|
3293 <xsl:text> this.reset_box(); |
|
3294 </xsl:text> |
|
3295 <xsl:text> // Put the button back in place |
|
3296 </xsl:text> |
|
3297 <xsl:text> this.element.appendChild(this.button_elt); |
|
3298 </xsl:text> |
|
3299 <xsl:text> // Mark as closed (to allow dispatch) |
|
3300 </xsl:text> |
|
3301 <xsl:text> this.opened = false; |
|
3302 </xsl:text> |
|
3303 <xsl:text> // Dispatch last cached value |
|
3304 </xsl:text> |
|
3305 <xsl:text> this.apply_cache(); |
|
3306 </xsl:text> |
|
3307 <xsl:text> } |
|
3308 </xsl:text> |
|
3309 <xsl:text> // Set text content when content is smaller than menu (no scrolling) |
|
3310 </xsl:text> |
|
3311 <xsl:text> set_complete_text(){ |
|
3312 </xsl:text> |
|
3313 <xsl:text> let spans = this.text_elt.children; |
|
3314 </xsl:text> |
|
3315 <xsl:text> let c = 0; |
|
3316 </xsl:text> |
|
3317 <xsl:text> for(let item of this.content){ |
|
3318 </xsl:text> |
|
3319 <xsl:text> let span=spans[c]; |
|
3320 </xsl:text> |
|
3321 <xsl:text> span.textContent = item; |
|
3322 </xsl:text> |
|
3323 <xsl:text> span.onclick = (evt) => this.bound_on_selection_click(c); |
|
3324 </xsl:text> |
|
3325 <xsl:text> c++; |
|
3326 </xsl:text> |
|
3327 <xsl:text> } |
|
3328 </xsl:text> |
|
3329 <xsl:text> } |
|
3330 </xsl:text> |
|
3331 <xsl:text> // Move partial view : |
|
3332 </xsl:text> |
|
3333 <xsl:text> // false : upward, lower value |
|
3334 </xsl:text> |
|
3335 <xsl:text> // true : downward, higher value |
|
3336 </xsl:text> |
|
3337 <xsl:text> scroll(forward){ |
|
3338 </xsl:text> |
|
3339 <xsl:text> let contentlength = this.content.length; |
|
3340 </xsl:text> |
|
3341 <xsl:text> let spans = this.text_elt.children; |
|
3342 </xsl:text> |
|
3343 <xsl:text> let spanslength = spans.length; |
|
3344 </xsl:text> |
|
3345 <xsl:text> // reduce accounted menu size according to jumps |
|
3346 </xsl:text> |
|
3347 <xsl:text> if(this.menu_offset != 0) spanslength--; |
|
3348 </xsl:text> |
|
3349 <xsl:text> if(this.menu_offset < contentlength - 1) spanslength--; |
|
3350 </xsl:text> |
|
3351 <xsl:text> if(forward){ |
|
3352 </xsl:text> |
|
3353 <xsl:text> this.menu_offset = Math.min( |
|
3354 </xsl:text> |
|
3355 <xsl:text> contentlength - spans.length + 1, |
|
3356 </xsl:text> |
|
3357 <xsl:text> this.menu_offset + spanslength); |
|
3358 </xsl:text> |
|
3359 <xsl:text> }else{ |
|
3360 </xsl:text> |
|
3361 <xsl:text> this.menu_offset = Math.max( |
|
3362 </xsl:text> |
|
3363 <xsl:text> 0, |
|
3364 </xsl:text> |
|
3365 <xsl:text> this.menu_offset - spanslength); |
|
3366 </xsl:text> |
|
3367 <xsl:text> } |
|
3368 </xsl:text> |
|
3369 <xsl:text> this.set_partial_text(); |
|
3370 </xsl:text> |
|
3371 <xsl:text> } |
|
3372 </xsl:text> |
|
3373 <xsl:text> // Setup partial view text content |
|
3374 </xsl:text> |
|
3375 <xsl:text> // with jumps at first and last entry when appropriate |
|
3376 </xsl:text> |
|
3377 <xsl:text> set_partial_text(){ |
|
3378 </xsl:text> |
|
3379 <xsl:text> let spans = this.text_elt.children; |
|
3380 </xsl:text> |
|
3381 <xsl:text> let contentlength = this.content.length; |
|
3382 </xsl:text> |
|
3383 <xsl:text> let spanslength = spans.length; |
|
3384 </xsl:text> |
|
3385 <xsl:text> let i = this.menu_offset, c = 0; |
|
3386 </xsl:text> |
|
3387 <xsl:text> while(c < spanslength){ |
|
3388 </xsl:text> |
|
3389 <xsl:text> let span=spans[c]; |
|
3390 </xsl:text> |
|
3391 <xsl:text> // backward jump only present if not exactly at start |
|
3392 </xsl:text> |
|
3393 <xsl:text> if(c == 0 && i != 0){ |
|
3394 </xsl:text> |
|
3395 <xsl:text> span.textContent = "↑ ↑ ↑"; |
|
3396 </xsl:text> |
|
3397 <xsl:text> span.onclick = this.bound_on_backward_click; |
|
3398 </xsl:text> |
|
3399 <xsl:text> // presence of forward jump when not right at the end |
|
3400 </xsl:text> |
|
3401 <xsl:text> }else if(c == spanslength-1 && i < contentlength - 1){ |
|
3402 </xsl:text> |
|
3403 <xsl:text> span.textContent = "↓ ↓ ↓"; |
|
3404 </xsl:text> |
|
3405 <xsl:text> span.onclick = this.bound_on_forward_click; |
|
3406 </xsl:text> |
|
3407 <xsl:text> // otherwise normal content |
|
3408 </xsl:text> |
|
3409 <xsl:text> }else{ |
|
3410 </xsl:text> |
|
3411 <xsl:text> span.textContent = this.content[i]; |
|
3412 </xsl:text> |
|
3413 <xsl:text> let sel = i; |
|
3414 </xsl:text> |
|
3415 <xsl:text> span.onclick = (evt) => this.bound_on_selection_click(sel); |
|
3416 </xsl:text> |
|
3417 <xsl:text> i++; |
|
3418 </xsl:text> |
|
3419 <xsl:text> } |
|
3420 </xsl:text> |
|
3421 <xsl:text> c++; |
|
3422 </xsl:text> |
|
3423 <xsl:text> } |
|
3424 </xsl:text> |
|
3425 <xsl:text> } |
|
3426 </xsl:text> |
|
3427 <xsl:text> open(){ |
|
3428 </xsl:text> |
|
3429 <xsl:text> let length = this.content.length; |
|
3430 </xsl:text> |
|
3431 <xsl:text> // systematically reset text, to strip eventual whitespace spans |
|
3432 </xsl:text> |
|
3433 <xsl:text> this.reset_text(); |
|
3434 </xsl:text> |
|
3435 <xsl:text> // grow as much as needed or possible |
|
3436 </xsl:text> |
|
3437 <xsl:text> let slots = this.grow_text(length); |
|
3438 </xsl:text> |
|
3439 <xsl:text> // Depending on final size |
|
3440 </xsl:text> |
|
3441 <xsl:text> if(slots == length) { |
|
3442 </xsl:text> |
|
3443 <xsl:text> // show all at once |
|
3444 </xsl:text> |
|
3445 <xsl:text> this.set_complete_text(); |
|
3446 </xsl:text> |
|
3447 <xsl:text> } else { |
|
3448 </xsl:text> |
|
3449 <xsl:text> // eventualy align menu to current selection, compensating for lift |
|
3450 </xsl:text> |
|
3451 <xsl:text> let offset = this.last_selection - this.lift; |
|
3452 </xsl:text> |
|
3453 <xsl:text> if(offset > 0) |
|
3454 </xsl:text> |
|
3455 <xsl:text> this.menu_offset = Math.min(offset + 1, length - slots + 1); |
|
3456 </xsl:text> |
|
3457 <xsl:text> else |
|
3458 </xsl:text> |
|
3459 <xsl:text> this.menu_offset = 0; |
|
3460 </xsl:text> |
|
3461 <xsl:text> // show surrounding values |
|
3462 </xsl:text> |
|
3463 <xsl:text> this.set_partial_text(); |
|
3464 </xsl:text> |
|
3465 <xsl:text> } |
|
3466 </xsl:text> |
|
3467 <xsl:text> // Now that text size is known, we can set the box around it |
|
3468 </xsl:text> |
|
3469 <xsl:text> this.adjust_box_to_text(); |
|
3470 </xsl:text> |
|
3471 <xsl:text> // Take button out until menu closed |
|
3472 </xsl:text> |
|
3473 <xsl:text> this.element.removeChild(this.button_elt); |
|
3474 </xsl:text> |
|
3475 <xsl:text> // Rise widget to top by moving it to last position among siblings |
|
3476 </xsl:text> |
|
3477 <xsl:text> this.element.parentNode.appendChild(this.element.parentNode.removeChild(this.element)); |
|
3478 </xsl:text> |
|
3479 <xsl:text> // disable interaction with background |
|
3480 </xsl:text> |
|
3481 <xsl:text> svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true); |
|
3482 </xsl:text> |
|
3483 <xsl:text> // mark as open |
|
3484 </xsl:text> |
|
3485 <xsl:text> this.opened = true; |
|
3486 </xsl:text> |
|
3487 <xsl:text> } |
|
3488 </xsl:text> |
|
3489 <xsl:text> // Put text element in normalized state |
|
3490 </xsl:text> |
|
3491 <xsl:text> reset_text(){ |
|
3492 </xsl:text> |
|
3493 <xsl:text> let txt = this.text_elt; |
|
3494 </xsl:text> |
|
3495 <xsl:text> let first = txt.firstElementChild; |
|
3496 </xsl:text> |
|
3497 <xsl:text> // remove attribute eventually added to first text line while opening |
|
3498 </xsl:text> |
|
3499 <xsl:text> first.removeAttribute("onclick"); |
|
3500 </xsl:text> |
|
3501 <xsl:text> first.removeAttribute("dy"); |
|
3502 </xsl:text> |
|
3503 <xsl:text> // keep only the first line of text |
|
3504 </xsl:text> |
|
3505 <xsl:text> for(let span of Array.from(txt.children).slice(1)){ |
|
3506 </xsl:text> |
|
3507 <xsl:text> txt.removeChild(span) |
|
3508 </xsl:text> |
|
3509 <xsl:text> } |
|
3510 </xsl:text> |
|
3511 <xsl:text> } |
|
3512 </xsl:text> |
|
3513 <xsl:text> // Put rectangle element in saved original state |
|
3514 </xsl:text> |
|
3515 <xsl:text> reset_box(){ |
|
3516 </xsl:text> |
|
3517 <xsl:text> let m = this.box_bbox; |
|
3518 </xsl:text> |
|
3519 <xsl:text> let b = this.box_elt; |
|
3520 </xsl:text> |
|
3521 <xsl:text> b.x.baseVal.value = m.x; |
|
3522 </xsl:text> |
|
3523 <xsl:text> b.y.baseVal.value = m.y; |
|
3524 </xsl:text> |
|
3525 <xsl:text> b.width.baseVal.value = m.width; |
|
3526 </xsl:text> |
|
3527 <xsl:text> b.height.baseVal.value = m.height; |
|
3528 </xsl:text> |
|
3529 <xsl:text> } |
|
3530 </xsl:text> |
|
3531 <xsl:text> // Use margin and text size to compute box size |
|
3532 </xsl:text> |
|
3533 <xsl:text> adjust_box_to_text(){ |
|
3534 </xsl:text> |
|
3535 <xsl:text> let [lmargin, tmargin] = this.margins; |
|
3536 </xsl:text> |
|
3537 <xsl:text> let m = this.text_elt.getBBox(); |
|
3538 </xsl:text> |
|
3539 <xsl:text> let b = this.box_elt; |
|
3540 </xsl:text> |
|
3541 <xsl:text> b.x.baseVal.value = m.x - lmargin; |
|
3542 </xsl:text> |
|
3543 <xsl:text> b.y.baseVal.value = m.y - tmargin; |
|
3544 </xsl:text> |
|
3545 <xsl:text> b.width.baseVal.value = 2 * lmargin + m.width; |
|
3546 </xsl:text> |
|
3547 <xsl:text> b.height.baseVal.value = 2 * tmargin + m.height; |
|
3548 </xsl:text> |
|
3549 <xsl:text> } |
|
3550 </xsl:text> |
|
3551 <xsl:text> } |
|
3552 </xsl:text> |
|
3553 </xsl:template> |
3066 <xsl:template mode="widget_defs" match="widget[@type='DropDown']"> |
3554 <xsl:template mode="widget_defs" match="widget[@type='DropDown']"> |
3067 <xsl:param name="hmi_element"/> |
3555 <xsl:param name="hmi_element"/> |
3068 <xsl:call-template name="defs_by_labels"> |
3556 <xsl:call-template name="defs_by_labels"> |
3069 <xsl:with-param name="hmi_element" select="$hmi_element"/> |
3557 <xsl:with-param name="hmi_element" select="$hmi_element"/> |
3070 <xsl:with-param name="labels"> |
3558 <xsl:with-param name="labels"> |
3071 <xsl:text>text box button</xsl:text> |
3559 <xsl:text>text box button</xsl:text> |
3072 </xsl:with-param> |
3560 </xsl:with-param> |
3073 </xsl:call-template> |
3561 </xsl:call-template> |
3074 <xsl:text> dispatch: function(value) { |
3562 <xsl:text> // It is assumed that list content conforms to Array interface. |
3075 </xsl:text> |
3563 </xsl:text> |
3076 <xsl:text> if(!this.opened) this.set_selection(value); |
3564 <xsl:text> content: [ |
3077 </xsl:text> |
|
3078 <xsl:text> }, |
|
3079 </xsl:text> |
|
3080 <xsl:text> init: function() { |
|
3081 </xsl:text> |
|
3082 <xsl:text> this.button_elt.setAttribute("onclick", "hmi_widgets['</xsl:text> |
|
3083 <xsl:value-of select="$hmi_element/@id"/> |
|
3084 <xsl:text>'].on_button_click()"); |
|
3085 </xsl:text> |
|
3086 <xsl:text> // Save original size of rectangle |
|
3087 </xsl:text> |
|
3088 <xsl:text> this.box_bbox = this.box_elt.getBBox() |
|
3089 </xsl:text> |
|
3090 <xsl:text> |
|
3091 </xsl:text> |
|
3092 <xsl:text> // Compute margins |
|
3093 </xsl:text> |
|
3094 <xsl:text> text_bbox = this.text_elt.getBBox() |
|
3095 </xsl:text> |
|
3096 <xsl:text> lmargin = text_bbox.x - this.box_bbox.x; |
|
3097 </xsl:text> |
|
3098 <xsl:text> tmargin = text_bbox.y - this.box_bbox.y; |
|
3099 </xsl:text> |
|
3100 <xsl:text> this.margins = [lmargin, tmargin].map(x => Math.max(x,0)); |
|
3101 </xsl:text> |
|
3102 <xsl:text> |
|
3103 </xsl:text> |
|
3104 <xsl:text> // It is assumed that list content conforms to Array interface. |
|
3105 </xsl:text> |
|
3106 <xsl:text> this.content = [ |
|
3107 </xsl:text> |
3565 </xsl:text> |
3108 <xsl:for-each select="arg"> |
3566 <xsl:for-each select="arg"> |
3109 <xsl:text>"</xsl:text> |
3567 <xsl:text>"</xsl:text> |
3110 <xsl:value-of select="@value"/> |
3568 <xsl:value-of select="@value"/> |
3111 <xsl:text>", |
3569 <xsl:text>", |
3112 </xsl:text> |
3570 </xsl:text> |
3113 </xsl:for-each> |
3571 </xsl:for-each> |
3114 <xsl:text> ]; |
3572 <xsl:text> ], |
3115 </xsl:text> |
3573 </xsl:text> |
3116 <xsl:text> |
3574 <xsl:text> |
3117 </xsl:text> |
|
3118 <xsl:text> // Index of first visible element in the menu, when opened |
|
3119 </xsl:text> |
|
3120 <xsl:text> this.menu_offset = 0; |
|
3121 </xsl:text> |
|
3122 <xsl:text> |
|
3123 </xsl:text> |
|
3124 <xsl:text> // How mutch to lift the menu vertically so that it does not cross bottom border |
|
3125 </xsl:text> |
|
3126 <xsl:text> this.lift = 0; |
|
3127 </xsl:text> |
|
3128 <xsl:text> |
|
3129 </xsl:text> |
|
3130 <xsl:text> // Event handlers cannot be object method ('this' is unknown) |
|
3131 </xsl:text> |
|
3132 <xsl:text> // as a workaround, handler given to addEventListener is bound in advance. |
|
3133 </xsl:text> |
|
3134 <xsl:text> this.bound_close_on_click_elsewhere = this.close_on_click_elsewhere.bind(this); |
|
3135 </xsl:text> |
|
3136 <xsl:text> |
|
3137 </xsl:text> |
|
3138 <xsl:text> this.opened = false; |
|
3139 </xsl:text> |
|
3140 <xsl:text> }, |
|
3141 </xsl:text> |
|
3142 <xsl:text> // Called when a menu entry is clicked |
|
3143 </xsl:text> |
|
3144 <xsl:text> on_selection_click: function(selection) { |
|
3145 </xsl:text> |
|
3146 <xsl:text> this.close(); |
|
3147 </xsl:text> |
|
3148 <xsl:text> this.apply_hmi_value(0, selection); |
|
3149 </xsl:text> |
|
3150 <xsl:text> }, |
|
3151 </xsl:text> |
|
3152 <xsl:text> on_button_click: function() { |
|
3153 </xsl:text> |
|
3154 <xsl:text> this.open(); |
|
3155 </xsl:text> |
|
3156 <xsl:text> }, |
|
3157 </xsl:text> |
|
3158 <xsl:text> on_backward_click: function(){ |
|
3159 </xsl:text> |
|
3160 <xsl:text> this.scroll(false); |
|
3161 </xsl:text> |
|
3162 <xsl:text> }, |
|
3163 </xsl:text> |
|
3164 <xsl:text> on_forward_click:function(){ |
|
3165 </xsl:text> |
|
3166 <xsl:text> this.scroll(true); |
|
3167 </xsl:text> |
|
3168 <xsl:text> }, |
|
3169 </xsl:text> |
|
3170 <xsl:text> set_selection: function(value) { |
|
3171 </xsl:text> |
|
3172 <xsl:text> let display_str; |
|
3173 </xsl:text> |
|
3174 <xsl:text> if(value >= 0 && value < this.content.length){ |
|
3175 </xsl:text> |
|
3176 <xsl:text> // if valid selection resolve content |
|
3177 </xsl:text> |
|
3178 <xsl:text> display_str = this.content[value]; |
|
3179 </xsl:text> |
|
3180 <xsl:text> this.last_selection = value; |
|
3181 </xsl:text> |
|
3182 <xsl:text> } else { |
|
3183 </xsl:text> |
|
3184 <xsl:text> // otherwise show problem |
|
3185 </xsl:text> |
|
3186 <xsl:text> display_str = "?"+String(value)+"?"; |
|
3187 </xsl:text> |
|
3188 <xsl:text> } |
|
3189 </xsl:text> |
|
3190 <xsl:text> // It is assumed that first span always stays, |
|
3191 </xsl:text> |
|
3192 <xsl:text> // and contains selection when menu is closed |
|
3193 </xsl:text> |
|
3194 <xsl:text> this.text_elt.firstElementChild.textContent = display_str; |
|
3195 </xsl:text> |
|
3196 <xsl:text> }, |
|
3197 </xsl:text> |
|
3198 <xsl:text> grow_text: function(up_to) { |
|
3199 </xsl:text> |
|
3200 <xsl:text> let count = 1; |
|
3201 </xsl:text> |
|
3202 <xsl:text> let txt = this.text_elt; |
|
3203 </xsl:text> |
|
3204 <xsl:text> let first = txt.firstElementChild; |
|
3205 </xsl:text> |
|
3206 <xsl:text> // Real world (pixels) boundaries of current page |
|
3207 </xsl:text> |
|
3208 <xsl:text> let bounds = svg_root.getBoundingClientRect(); |
|
3209 </xsl:text> |
|
3210 <xsl:text> this.lift = 0; |
|
3211 </xsl:text> |
|
3212 <xsl:text> while(count < up_to) { |
|
3213 </xsl:text> |
|
3214 <xsl:text> let next = first.cloneNode(); |
|
3215 </xsl:text> |
|
3216 <xsl:text> // relative line by line text flow instead of absolute y coordinate |
|
3217 </xsl:text> |
|
3218 <xsl:text> next.removeAttribute("y"); |
|
3219 </xsl:text> |
|
3220 <xsl:text> next.setAttribute("dy", "1.1em"); |
|
3221 </xsl:text> |
|
3222 <xsl:text> // default content to allow computing text element bbox |
|
3223 </xsl:text> |
|
3224 <xsl:text> next.textContent = "..."; |
|
3225 </xsl:text> |
|
3226 <xsl:text> // append new span to text element |
|
3227 </xsl:text> |
|
3228 <xsl:text> txt.appendChild(next); |
|
3229 </xsl:text> |
|
3230 <xsl:text> // now check if text extended by one row fits to page |
|
3231 </xsl:text> |
|
3232 <xsl:text> // FIXME : exclude margins to be more accurate on box size |
|
3233 </xsl:text> |
|
3234 <xsl:text> let rect = txt.getBoundingClientRect(); |
|
3235 </xsl:text> |
|
3236 <xsl:text> if(rect.bottom > bounds.bottom){ |
|
3237 </xsl:text> |
|
3238 <xsl:text> // in case of overflow at the bottom, lift up one row |
|
3239 </xsl:text> |
|
3240 <xsl:text> let backup = first.getAttribute("dy"); |
|
3241 </xsl:text> |
|
3242 <xsl:text> // apply lift asr a dy added too first span (y attrib stays) |
|
3243 </xsl:text> |
|
3244 <xsl:text> first.setAttribute("dy", "-"+String((this.lift+1)*1.1)+"em"); |
|
3245 </xsl:text> |
|
3246 <xsl:text> rect = txt.getBoundingClientRect(); |
|
3247 </xsl:text> |
|
3248 <xsl:text> if(rect.top > bounds.top){ |
|
3249 </xsl:text> |
|
3250 <xsl:text> this.lift += 1; |
|
3251 </xsl:text> |
|
3252 <xsl:text> } else { |
|
3253 </xsl:text> |
|
3254 <xsl:text> // if it goes over the top, then backtrack |
|
3255 </xsl:text> |
|
3256 <xsl:text> // restore dy attribute on first span |
|
3257 </xsl:text> |
|
3258 <xsl:text> if(backup) |
|
3259 </xsl:text> |
|
3260 <xsl:text> first.setAttribute("dy", backup); |
|
3261 </xsl:text> |
|
3262 <xsl:text> else |
|
3263 </xsl:text> |
|
3264 <xsl:text> first.removeAttribute("dy"); |
|
3265 </xsl:text> |
|
3266 <xsl:text> // remove unwanted child |
|
3267 </xsl:text> |
|
3268 <xsl:text> txt.removeChild(next); |
|
3269 </xsl:text> |
|
3270 <xsl:text> return count; |
|
3271 </xsl:text> |
|
3272 <xsl:text> } |
|
3273 </xsl:text> |
|
3274 <xsl:text> } |
|
3275 </xsl:text> |
|
3276 <xsl:text> count++; |
|
3277 </xsl:text> |
|
3278 <xsl:text> } |
|
3279 </xsl:text> |
|
3280 <xsl:text> return count; |
|
3281 </xsl:text> |
|
3282 <xsl:text> }, |
|
3283 </xsl:text> |
|
3284 <xsl:text> close_on_click_elsewhere: function(e) { |
|
3285 </xsl:text> |
|
3286 <xsl:text> // inhibit events not targetting spans (menu items) |
|
3287 </xsl:text> |
|
3288 <xsl:text> if(e.target.parentNode !== this.text_elt){ |
|
3289 </xsl:text> |
|
3290 <xsl:text> e.stopPropagation(); |
|
3291 </xsl:text> |
|
3292 <xsl:text> // close menu in case click is outside box |
|
3293 </xsl:text> |
|
3294 <xsl:text> if(e.target !== this.box_elt) |
|
3295 </xsl:text> |
|
3296 <xsl:text> this.close(); |
|
3297 </xsl:text> |
|
3298 <xsl:text> } |
|
3299 </xsl:text> |
|
3300 <xsl:text> }, |
|
3301 </xsl:text> |
|
3302 <xsl:text> close: function(){ |
|
3303 </xsl:text> |
|
3304 <xsl:text> // Stop hogging all click events |
|
3305 </xsl:text> |
|
3306 <xsl:text> svg_root.removeEventListener("click", this.bound_close_on_click_elsewhere, true); |
|
3307 </xsl:text> |
|
3308 <xsl:text> // Restore position and sixe of widget elements |
|
3309 </xsl:text> |
|
3310 <xsl:text> this.reset_text(); |
|
3311 </xsl:text> |
|
3312 <xsl:text> this.reset_box(); |
|
3313 </xsl:text> |
|
3314 <xsl:text> // Put the button back in place |
|
3315 </xsl:text> |
|
3316 <xsl:text> this.element.appendChild(this.button_elt); |
|
3317 </xsl:text> |
|
3318 <xsl:text> // Mark as closed (to allow dispatch) |
|
3319 </xsl:text> |
|
3320 <xsl:text> this.opened = false; |
|
3321 </xsl:text> |
|
3322 <xsl:text> // Dispatch last cached value |
|
3323 </xsl:text> |
|
3324 <xsl:text> this.apply_cache(); |
|
3325 </xsl:text> |
|
3326 <xsl:text> }, |
|
3327 </xsl:text> |
|
3328 <xsl:text> // Set text content when content is smaller than menu (no scrolling) |
|
3329 </xsl:text> |
|
3330 <xsl:text> set_complete_text: function(){ |
|
3331 </xsl:text> |
|
3332 <xsl:text> let spans = this.text_elt.children; |
|
3333 </xsl:text> |
|
3334 <xsl:text> let c = 0; |
|
3335 </xsl:text> |
|
3336 <xsl:text> for(let item of this.content){ |
|
3337 </xsl:text> |
|
3338 <xsl:text> let span=spans[c]; |
|
3339 </xsl:text> |
|
3340 <xsl:text> span.textContent = item; |
|
3341 </xsl:text> |
|
3342 <xsl:text> span.setAttribute("onclick", "hmi_widgets['</xsl:text> |
|
3343 <xsl:value-of select="$hmi_element/@id"/> |
|
3344 <xsl:text>'].on_selection_click("+c+")"); |
|
3345 </xsl:text> |
|
3346 <xsl:text> c++; |
|
3347 </xsl:text> |
|
3348 <xsl:text> } |
|
3349 </xsl:text> |
|
3350 <xsl:text> }, |
|
3351 </xsl:text> |
|
3352 <xsl:text> // Move partial view : |
|
3353 </xsl:text> |
|
3354 <xsl:text> // false : upward, lower value |
|
3355 </xsl:text> |
|
3356 <xsl:text> // true : downward, higher value |
|
3357 </xsl:text> |
|
3358 <xsl:text> scroll: function(forward){ |
|
3359 </xsl:text> |
|
3360 <xsl:text> let contentlength = this.content.length; |
|
3361 </xsl:text> |
|
3362 <xsl:text> let spans = this.text_elt.children; |
|
3363 </xsl:text> |
|
3364 <xsl:text> let spanslength = spans.length; |
|
3365 </xsl:text> |
|
3366 <xsl:text> // reduce accounted menu size according to jumps |
|
3367 </xsl:text> |
|
3368 <xsl:text> if(this.menu_offset != 0) spanslength--; |
|
3369 </xsl:text> |
|
3370 <xsl:text> if(this.menu_offset < contentlength - 1) spanslength--; |
|
3371 </xsl:text> |
|
3372 <xsl:text> if(forward){ |
|
3373 </xsl:text> |
|
3374 <xsl:text> this.menu_offset = Math.min( |
|
3375 </xsl:text> |
|
3376 <xsl:text> contentlength - spans.length + 1, |
|
3377 </xsl:text> |
|
3378 <xsl:text> this.menu_offset + spanslength); |
|
3379 </xsl:text> |
|
3380 <xsl:text> }else{ |
|
3381 </xsl:text> |
|
3382 <xsl:text> this.menu_offset = Math.max( |
|
3383 </xsl:text> |
|
3384 <xsl:text> 0, |
|
3385 </xsl:text> |
|
3386 <xsl:text> this.menu_offset - spanslength); |
|
3387 </xsl:text> |
|
3388 <xsl:text> } |
|
3389 </xsl:text> |
|
3390 <xsl:text> this.set_partial_text(); |
|
3391 </xsl:text> |
|
3392 <xsl:text> }, |
|
3393 </xsl:text> |
|
3394 <xsl:text> // Setup partial view text content |
|
3395 </xsl:text> |
|
3396 <xsl:text> // with jumps at first and last entry when appropriate |
|
3397 </xsl:text> |
|
3398 <xsl:text> set_partial_text: function(){ |
|
3399 </xsl:text> |
|
3400 <xsl:text> let spans = this.text_elt.children; |
|
3401 </xsl:text> |
|
3402 <xsl:text> let contentlength = this.content.length; |
|
3403 </xsl:text> |
|
3404 <xsl:text> let spanslength = spans.length; |
|
3405 </xsl:text> |
|
3406 <xsl:text> let i = this.menu_offset, c = 0; |
|
3407 </xsl:text> |
|
3408 <xsl:text> while(c < spanslength){ |
|
3409 </xsl:text> |
|
3410 <xsl:text> let span=spans[c]; |
|
3411 </xsl:text> |
|
3412 <xsl:text> // backward jump only present if not exactly at start |
|
3413 </xsl:text> |
|
3414 <xsl:text> if(c == 0 && i != 0){ |
|
3415 </xsl:text> |
|
3416 <xsl:text> span.textContent = "↑ ↑ ↑"; |
|
3417 </xsl:text> |
|
3418 <xsl:text> span.setAttribute("onclick", "hmi_widgets['</xsl:text> |
|
3419 <xsl:value-of select="$hmi_element/@id"/> |
|
3420 <xsl:text>'].on_backward_click()"); |
|
3421 </xsl:text> |
|
3422 <xsl:text> // presence of forward jump when not right at the end |
|
3423 </xsl:text> |
|
3424 <xsl:text> }else if(c == spanslength-1 && i < contentlength - 1){ |
|
3425 </xsl:text> |
|
3426 <xsl:text> span.textContent = "↓ ↓ ↓"; |
|
3427 </xsl:text> |
|
3428 <xsl:text> span.setAttribute("onclick", "hmi_widgets['</xsl:text> |
|
3429 <xsl:value-of select="$hmi_element/@id"/> |
|
3430 <xsl:text>'].on_forward_click()"); |
|
3431 </xsl:text> |
|
3432 <xsl:text> // otherwise normal content |
|
3433 </xsl:text> |
|
3434 <xsl:text> }else{ |
|
3435 </xsl:text> |
|
3436 <xsl:text> span.textContent = this.content[i]; |
|
3437 </xsl:text> |
|
3438 <xsl:text> span.setAttribute("onclick", "hmi_widgets['</xsl:text> |
|
3439 <xsl:value-of select="$hmi_element/@id"/> |
|
3440 <xsl:text>'].on_selection_click("+i+")"); |
|
3441 </xsl:text> |
|
3442 <xsl:text> i++; |
|
3443 </xsl:text> |
|
3444 <xsl:text> } |
|
3445 </xsl:text> |
|
3446 <xsl:text> c++; |
|
3447 </xsl:text> |
|
3448 <xsl:text> } |
|
3449 </xsl:text> |
|
3450 <xsl:text> }, |
|
3451 </xsl:text> |
|
3452 <xsl:text> open: function(){ |
|
3453 </xsl:text> |
|
3454 <xsl:text> let length = this.content.length; |
|
3455 </xsl:text> |
|
3456 <xsl:text> // systematically reset text, to strip eventual whitespace spans |
|
3457 </xsl:text> |
|
3458 <xsl:text> this.reset_text(); |
|
3459 </xsl:text> |
|
3460 <xsl:text> // grow as much as needed or possible |
|
3461 </xsl:text> |
|
3462 <xsl:text> let slots = this.grow_text(length); |
|
3463 </xsl:text> |
|
3464 <xsl:text> // Depending on final size |
|
3465 </xsl:text> |
|
3466 <xsl:text> if(slots == length) { |
|
3467 </xsl:text> |
|
3468 <xsl:text> // show all at once |
|
3469 </xsl:text> |
|
3470 <xsl:text> this.set_complete_text(); |
|
3471 </xsl:text> |
|
3472 <xsl:text> } else { |
|
3473 </xsl:text> |
|
3474 <xsl:text> // eventualy align menu to current selection, compensating for lift |
|
3475 </xsl:text> |
|
3476 <xsl:text> let offset = this.last_selection - this.lift; |
|
3477 </xsl:text> |
|
3478 <xsl:text> if(offset > 0) |
|
3479 </xsl:text> |
|
3480 <xsl:text> this.menu_offset = Math.min(offset + 1, length - slots + 1); |
|
3481 </xsl:text> |
|
3482 <xsl:text> else |
|
3483 </xsl:text> |
|
3484 <xsl:text> this.menu_offset = 0; |
|
3485 </xsl:text> |
|
3486 <xsl:text> // show surrounding values |
|
3487 </xsl:text> |
|
3488 <xsl:text> this.set_partial_text(); |
|
3489 </xsl:text> |
|
3490 <xsl:text> } |
|
3491 </xsl:text> |
|
3492 <xsl:text> // Now that text size is known, we can set the box around it |
|
3493 </xsl:text> |
|
3494 <xsl:text> this.adjust_box_to_text(); |
|
3495 </xsl:text> |
|
3496 <xsl:text> // Take button out until menu closed |
|
3497 </xsl:text> |
|
3498 <xsl:text> this.element.removeChild(this.button_elt); |
|
3499 </xsl:text> |
|
3500 <xsl:text> // Rise widget to top by moving it to last position among siblings |
|
3501 </xsl:text> |
|
3502 <xsl:text> this.element.parentNode.appendChild(this.element.parentNode.removeChild(this.element)); |
|
3503 </xsl:text> |
|
3504 <xsl:text> // disable interaction with background |
|
3505 </xsl:text> |
|
3506 <xsl:text> svg_root.addEventListener("click", this.bound_close_on_click_elsewhere, true); |
|
3507 </xsl:text> |
|
3508 <xsl:text> // mark as open |
|
3509 </xsl:text> |
|
3510 <xsl:text> this.opened = true; |
|
3511 </xsl:text> |
|
3512 <xsl:text> }, |
|
3513 </xsl:text> |
|
3514 <xsl:text> // Put text element in normalized state |
|
3515 </xsl:text> |
|
3516 <xsl:text> reset_text: function(){ |
|
3517 </xsl:text> |
|
3518 <xsl:text> let txt = this.text_elt; |
|
3519 </xsl:text> |
|
3520 <xsl:text> let first = txt.firstElementChild; |
|
3521 </xsl:text> |
|
3522 <xsl:text> // remove attribute eventually added to first text line while opening |
|
3523 </xsl:text> |
|
3524 <xsl:text> first.removeAttribute("onclick"); |
|
3525 </xsl:text> |
|
3526 <xsl:text> first.removeAttribute("dy"); |
|
3527 </xsl:text> |
|
3528 <xsl:text> // keep only the first line of text |
|
3529 </xsl:text> |
|
3530 <xsl:text> for(let span of Array.from(txt.children).slice(1)){ |
|
3531 </xsl:text> |
|
3532 <xsl:text> txt.removeChild(span) |
|
3533 </xsl:text> |
|
3534 <xsl:text> } |
|
3535 </xsl:text> |
|
3536 <xsl:text> }, |
|
3537 </xsl:text> |
|
3538 <xsl:text> // Put rectangle element in saved original state |
|
3539 </xsl:text> |
|
3540 <xsl:text> reset_box: function(){ |
|
3541 </xsl:text> |
|
3542 <xsl:text> let m = this.box_bbox; |
|
3543 </xsl:text> |
|
3544 <xsl:text> let b = this.box_elt; |
|
3545 </xsl:text> |
|
3546 <xsl:text> b.x.baseVal.value = m.x; |
|
3547 </xsl:text> |
|
3548 <xsl:text> b.y.baseVal.value = m.y; |
|
3549 </xsl:text> |
|
3550 <xsl:text> b.width.baseVal.value = m.width; |
|
3551 </xsl:text> |
|
3552 <xsl:text> b.height.baseVal.value = m.height; |
|
3553 </xsl:text> |
|
3554 <xsl:text> }, |
|
3555 </xsl:text> |
|
3556 <xsl:text> // Use margin and text size to compute box size |
|
3557 </xsl:text> |
|
3558 <xsl:text> adjust_box_to_text: function(){ |
|
3559 </xsl:text> |
|
3560 <xsl:text> let [lmargin, tmargin] = this.margins; |
|
3561 </xsl:text> |
|
3562 <xsl:text> let m = this.text_elt.getBBox(); |
|
3563 </xsl:text> |
|
3564 <xsl:text> let b = this.box_elt; |
|
3565 </xsl:text> |
|
3566 <xsl:text> b.x.baseVal.value = m.x - lmargin; |
|
3567 </xsl:text> |
|
3568 <xsl:text> b.y.baseVal.value = m.y - tmargin; |
|
3569 </xsl:text> |
|
3570 <xsl:text> b.width.baseVal.value = 2 * lmargin + m.width; |
|
3571 </xsl:text> |
|
3572 <xsl:text> b.height.baseVal.value = 2 * tmargin + m.height; |
|
3573 </xsl:text> |
|
3574 <xsl:text> }, |
|
3575 </xsl:text> |
3575 </xsl:text> |
3576 </xsl:template> |
3576 </xsl:template> |
3577 <xsl:template mode="widget_defs" match="widget[@type='ForEach']"> |
3577 <xsl:template mode="widget_defs" match="widget[@type='ForEach']"> |
3578 <xsl:param name="hmi_element"/> |
3578 <xsl:param name="hmi_element"/> |
3579 <xsl:if test="count(path) != 1"> |
3579 <xsl:if test="count(path) != 1"> |