All files / src/custom-element location-element.js

100% Statements 67/67
97.14% Branches 34/35
100% Functions 18/18
100% Lines 64/64

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 11055x         46x 44x 2x 2x   10x 10x   4x 4x         4x 1x 1x 1x 1x 1x             4x                     18x 18x 18x 18x 55x 55x 46x 55x   55x 55x 55x 26x   55x 55x 770x 513x   55x 55x   18x   51x   41x 41x 41x 12x 1x 1x 11x 7x   51x 5x 5x 5x 5x 5x   51x 51x   18x   2x 2x 2x 2x           63x 33x 16x 47x     4x 2x     4x  
const attr = ( el, attr )=> el.getAttribute( attr );
 
let originalHistory;
 
function ensureTrackLocationChange()
{   if( originalHistory )
        return;
    originalHistory = {};
    'back,forward,go,pushState,replaceState'.split(',').forEach( k =>
    {
        originalHistory[ k ] = history[ k ];
        history[ k ] = function(...rest )
        {
            originalHistory[k].apply( history, rest );
            window.dispatchEvent( new CustomEvent('dce-location',{detail:{ k }}) );
        }
    });
}
const methods =
{   'location.href'         : src => window.location.href = src
,   'location.hash'         : src => window.location.hash = src
,   'location.assign'       : src => window.location.assign( src )
,   'location.replace'      : src => window.location.replace( src )
,   'history.pushState'     : src => window.history.pushState( {}, "", src )
,   'history.replaceState'  : src => window.history.replaceState( {}, "", src )
};
 
 
export class LocationElement extends HTMLElement
{
    static observedAttributes=
            [   'value' // populated from url
            ,   'slice'
            ,   'href'  // url to be parsed. When omitted window.location is used.
            ,   'type' // `text|json`, defaults to text, other types are compatible with INPUT field
            ,   'live' // monitors history change, applicable only when href is omitted.
            ,   'src' // sets the URL
            ,   'method' // when defined, changes URL by one of predefined methods.
            ];
 
    constructor()
    {
        super();
        const      state = {}
        ,       listener = () => setTimeout( propagateSlice,1 )
        , propagateSlice = ()=>
        {   const urlStr = attr(this,'href')
            if(!urlStr)
                ensureTrackLocationChange();
            const url = urlStr? new URL(urlStr, window.location) : window.location;
 
            const params= {}
            const search = new URLSearchParams(url.search);
            for (const key of search.keys())
                params[key] = search.getAll(key)
 
            const detail = {params}
            for( const k in url )
            {   if ('string' === typeof url[k])
                    detail[k] = url[k]
            }
            this.value = detail;
            this.dispatchEvent( new Event('change') );
        };
        this.sliceInit = s =>
        {
            if( this.hasAttribute('method') )
            {
                const method = this.getAttribute('method');
                const src = this.getAttribute('src');
                if( method && src )
                    if( method === 'location.hash' )
                    {   Eif( src !== window.location.hash )
                            methods[ method ]?.( src );
                    }else if( window.location.href !== new URL(src, window.location).href )
                        methods[method]?.(src);
            }
            if( !state.listener && this.hasAttribute('live') )
            {   state.listener = 1;
                window.navigation?.addEventListener("navigate", listener );
                window.addEventListener( 'popstate'      , listener );
                window.addEventListener( 'hashchange'    , listener );
                window.addEventListener( 'dce-location'  , listener );
            }
            propagateSlice();
            return s || {}
        }
        this._destroy = ()=>
        {
            window.removeEventListener('popstate'    , listener);
            window.removeEventListener('hashchange'  , listener);
            window.removeEventListener('dce-location', listener);
            delete state.listener;
        };
 
    }
    attributeChangedCallback(name, oldValue, newValue)
    {
        if('href'!== name && 'method' !== name && 'src' )
            if( !['method','src','href'].includes(name) )
                return;
        this.sliceInit && this.sliceInit();
    }
 
    connectedCallback(){ this.sliceInit() }
    disconnectedCallback(){ this._destroy() }
}
 
window.customElements.define( 'location-element', LocationElement );
export default LocationElement;