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 11052x         42x 40x 2x 2x   10x 10x   4x 4x         4x 1x 1x 1x 1x 1x             4x                     18x 18x 18x 18x 52x 52x 42x 52x   52x 52x 52x 28x   52x 52x 728x 488x   52x 52x   18x   48x   37x 37x 37x 13x 1x 1x 12x 7x   48x 5x 5x 5x 5x 5x   48x 48x   18x   2x 2x 2x 2x           72x 39x 28x 44x     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;