photo-manager/static/photos/script.js

1404 lines
42 KiB
JavaScript
Raw Normal View History

2023-10-04 01:24:49 +02:00
'use strict'
// const { urlencoded } = require('body-parser')
const d = document
, w = window
let ProductTemplate = d.createElement( 'ProductTemplate' )
, ThumbnailsRowTemplate = d.createElement( 'ThumbnailsRowTemplate' )
, ThumbnailTemplate = d.createElement( 'ThumbnailTemplate' )
, ProductTypesDatalist = d.getElementById( 'product_types' )
, searchField = d.getElementById( 'search' )
, searchForm = d.querySelector( 'nav form' )
, btnShowAll = d.getElementById( 'ShowAll' )
, btnShowWithoutPhotos = d.getElementById( 'ShowWithoutPhotos' )
, chkShowOnlyAvaliable = d.getElementById( 'ShowOnlyAvaliable' )
, txtCurrentCount = d.getElementById( 'CurrentCount' )
, ProgressBarElement = d.querySelector( 'ProgressBarElement' )
, Main = d.getElementById( 'photos' )
, Moving_Thumbnail = null
, unloadedProducts = []
, ProductTypes = new Set( )
, ProgressBar
, ProgressBarTotal
, MaxChunk = 100
, BaseURL = localStorage.getItem( 'BaseURL' ) || ''
, Accounts_Ordered = JSON.parse( localStorage.getItem( 'Accounts_Ordered' ) || '[ ]' )
, Default_Accounts = JSON.parse( localStorage.getItem( 'Default_Accounts' ) || '{ }' )
, _HardReload_in_3_2_ = false
, Products = Object.assign( { },
...Object.keys( localStorage ).map( SKU =>
( ![ 'lastUpdated', 'Accounts_Ordered', 'Default_Accounts', 'BaseURL' ].includes( SKU ) )
&& ({ [ SKU ]: JSON.parse( localStorage.getItem( SKU ) || '{ }' ) })
|| ({ })
) )
//console.log( Products )
const ceil_abs = N => Math.ceil( Math.abs( N ) )
const range = ( cnt_or_start = 0, stop = null, step = 1 ) =>
( null === stop )
&& [...Array( cnt_or_start ).keys( )]
|| [...Array( ceil_abs( ( stop - cnt_or_start ) / step ) )]
.map( ( _, i ) => cnt_or_start + i * step )
function PopulateSearchWithTypes( )
{ [...ProductTypes]
.sort( ( a,b ) => a.localeCompare( b ) )
.map( T =>
{ let O = document.createElement( 'option' )
O.value = T
ProductTypesDatalist.appendChild( O )
})
}
const getHashFromPhotoURL = PhotoURL =>
PhotoURL.match( /([^\/_]+)_?L\.jpg$/ )[ 1 ]
const unURIHash = ( Hash = w.location.hash ) =>
decodeURIComponent( Hash )
const Now = ( ) =>
new Date( ).toISOString( )
.slice( 0, 19 )
.replace( 'T', ' ' )
const Product_Error =
{ SKU: 'ERR-01-001'
, Title: 'Cant get Products from server'
, Variants: { _default: { 10:'', 1:'' } }
, Accounts: { }
}
const DefaultRequestParams =
{ ContentType: 'application/json'
, url: '/api/photos/'
, method: 'GET'
, body: ''
}
const Search_onKeypress = e =>
{ if( ( typeof e.key == 'undefined' )
|| ( e.key == null )
)
{ return false }
const regexValidKeys = new RegExp
( '^'
+ '[µßáäåæëíïðñóöøúüþœœ\\w\\d]'
+ '|Backspace|Delete|Clear|Cut|Paste|Undo|Redo'
+ '$'
, 'i'
)
if( regexValidKeys.test( e.key ) )
{ showOnlySearched( ) }
}
const setURL = path =>
{ let u = new URL( w.location.toString( ) )
u.pathname = '/photos/' + path
u.hash = ''
u.search = ''
d.title = path
history.pushState( { search: path }, path, u.toString( ) )
}
const AddClassFor600ms = ( _DOMnode, _Class ) =>
{ _DOMnode.classList.add( _Class )
setTimeout( ( ) => { _DOMnode.classList.remove( _Class ) }
, 600
)
}
const getPhotoURL = ({ SKU, Hash, Size }) =>
( SKU && Hash && Size )
&& ( BaseURL
+ GetID( SKU )
+ '/'
+ Hash
+ ( Hash.length>2 ? '_' : '' )
+ Size
+ '.jpg'
)
|| ''
const getSearchIndex = ( ...args ) =>
[ ...args ]
.join( '' )
.replace( /[^µßáäåæëíïðñóöøúüþœœ\w\d]/gi,'' )
.toUpperCase( )
const setProgressBarTotal = Total =>
{ ProgressBarTotal = Total
ProgressBar = -1
incrementProgressBar( )
}
function incrementProgressBar( )
{ ProgressBarElement.style.width =
( ++ProgressBar * 100 / ProgressBarTotal ) + '%'
if( ProgressBar == ProgressBarTotal )
{ Window_onScroll( ) }
}
function collapseAll( )
{ setURL( searchField.value )
d.querySelectorAll( 'ProductElement.targeted' )
.forEach( P => P.classList.remove( 'targeted' ) )
}
function unhideAll( )
{ d.querySelectorAll( 'ProductElement.hidden' )
.forEach( P => P.classList.remove( 'hidden' ) )
}
const UpdateCounter = Count =>
{ CurrentCount.textContent
= Count
|| d.querySelectorAll( 'ProductElement:not(.hidden):not(.zero)' )
.length
}
const ShowOnlyAvaliable = e =>
{ const NonZero = Boolean( chkShowOnlyAvaliable.checked )
d.querySelectorAll( 'ProductElement' )
.forEach( P =>
{ if( NonZero && Products[ P.dataset.sku ].Amount == 0 )
{ P.classList.add('zero') }
else
{ P.classList.remove('zero') }
})
UpdateCounter( )
}
const ScrollTo = P =>
{ w.scroll( 0, P.offsetTop + P.offsetParent.offsetTop - 50 )
Window_onScroll( )
}
const obj2URLSearchParams = obj =>
{ let u = new URLSearchParams( )
for( const key in obj )
{ u.set( key, obj[ key ] ) }
return u.toString( )
}
const request = Params =>
new Promise( ( resolve, reject ) =>
{ let { url, method, body, ContentType } = DefaultRequestParams
if( 'string' == typeof Params )
{ url = Params }
else
{ if( 'string' == typeof Params.url )
{ url = Params.url }
else
{ const { api, query } = Params
let u = new URL( w.location.toString( ) )
u.pathname = '/api/photos/' + api
u.search = obj2URLSearchParams( query || {} )
u.hash = ''
url = u.toString( )
}
method = Params.method || method
body = Params.body || body
ContentType = Params.ContentType || ContentType
}
const headers = Params.headers
|| { 'Content-Type' : ContentType }
fetch( url
, [ 'GET','HEAD' ].includes( method )
? { headers, method }
: { headers, method, body }
)
.then( res =>
{ if( !res || !res.ok )
reject( res && res.status || url )
if( res.statusCode >= 400 )
resolve( false )
resolve( res.text( ) )
})
.catch( err =>
{ reject({ err, Params })
console.error( 'rejected request::fetch', err )
})
}).catch( err => console.error( 'cant [request]', err ) )
const GetID = SKU =>
SKU.replace( /\s+/g,'' )
.replace( /[^\w\d\.\,_-]/g,'-' )
.toUpperCase( )
const SendFiles = ({ Form, SKU, Variant, Nr }) =>
{ let u = new URL( w.location.toString( ) )
u.pathname = '/api/photos'
u.search = obj2URLSearchParams({ SKU, Variant, Nr: Nr||'0' })
Form.action = u.toString( )
Form.submit( )
}
const ProcessPhotoFile = ({ PhotoFile, Nr }) =>
{ let canvas = d.createElement( 'canvas' )
, ctx = canvas.getContext( '2d' )
, reader = new FileReader( )
, Hash
return new Promise( ( resolve,reject ) =>
{ reader.onload = e =>
{ let img = new Image( )
img.onload = ( ) =>
{ canvas.width = 200
canvas.height = 200
ctx.drawImage( img, 0, 0, 200, 200 )
let imageData = ctx.getImageData(0, 0, 200, 200)
if( 10 == Nr )
{ let data = imageData.data
for( let i = 0; i < data.length; i += 4)
{ data[ i ] // red
= data[ i+1 ] // green
= data[ i+2 ] // blue
= 0.34 * data[i] + 0.5 * data[i + 1] + 0.16 * data[i + 2]
}
ctx.putImageData( imageData, 0, 0 )
}
resolve({ imgData: canvas.toDataURL( 'image/jpeg' )
, imgWidth: img.naturalWidth
, imgHeight: img.naturalHeight
})
}
img.src = e.target.result
}
try
{ reader.readAsDataURL( PhotoFile ) }
catch( e )
{ reject( e ) }
} ).catch( err => console.error( 'cant [ProcessPhotoFile]', err ))
}
const UploadSinglePhoto = async e =>
{ let Form = e.target.parentNode
, T = Form.parentNode.parentNode
, R = T.parentNode
, P = R.parentNode.parentNode
, PhotoFile = e.target.files[ 0 ]
, SKU = P.dataset.sku
, Nr = T.dataset.nr
, Variant = R.dataset.variant
, Hash = T.dataset.hash
SendFiles({ Form, SKU, Variant, Nr })
T.dataset.src_big = getPhotoURL({ SKU, Hash, Size:'M' })
+ '?' + Date.now( )
const { imgData, imgWidth, imgHeight } = await ProcessPhotoFile({ PhotoFile, Nr })
T.dataset.src_small
= T.querySelector( 'img' ).src
= imgData
if( 1600 != imgWidth
|| 1600 != imgHeight
)
{ T.querySelector( 'WrongDimensions' ).innerText = imgWidth + 'x' + imgHeight }
updateAccounts( P )
}
const UploadMultiplePhotos = async e =>
{ let Form = e.target.parentNode
, R = Form.parentNode.parentNode.parentNode.parentNode
, P = R.parentNode.parentNode
, SKU = P.dataset.sku
, Variant = R.dataset.variant
, Photos = e.target.files
, TT = R.querySelectorAll( 'Thumbnail' )
, Nr = TT.length-1
SendFiles({ Form, SKU, Variant })
for( const i in Photos )
{ if( i != Number( i ) )
{ updateAccounts( P )
return true
}
Nr = ( 9 == Nr ? 11 : Nr + 1 )
if( 10 < Nr )
{ return false }
const Hash = ''// await getFilehash( Photos[i] )
, PhotoFile = Photos[i]
let T = createThumbnail(
{ SKU
, Nr
, Hash
, Variant
, PrevUUID: SKU + ':' + ( TT[ Nr-1 ] ? Nr-1 : TT.length-1 )
, NextUUID: SKU + ':' + ( Photos[ i+1 ] ? Nr+1 : 10 )
})
T.dataset.src_big = getPhotoURL({ SKU, Hash, Size:'M' })
const { imgData, imgWidth, imgHeight } =
await ProcessPhotoFile({ PhotoFile, Nr })
T.dataset.src_small
= T.querySelector( 'img' ).src
= imgData
if( 1600 != imgWidth
|| 1600 != imgHeight
)
{ T.querySelector( 'WrongDimensions' ).innerText = imgWidth + 'x' + imgHeight }
R.appendChild( T )
}
}
const VariantSelector_onChange = e =>
{ let S = e.target || e
, R = S.parentNode.parentNode
, P = R.parentNode.parentNode
const SKU = P.dataset.sku
, Account = R.dataset.account
, Variant = S.value
, Variants = Object.keys( Products[ SKU ].Variants )
Products[ SKU ].Accounts[ Account ] = Variant
request({ api: 'assignVariantToAccount'
, method: 'PUT'
, query: { SKU, Account, Variant }
})
R.parentNode
.replaceChild( createAccount(
{ SKU
, Account
, Variant
, Variants
, ProductElement: P
} )
, R )
}
const updateAccounts = ProductElement =>
{ ProductElement.querySelectorAll( 'Variants ThumbnailsRow' ).forEach( V =>
{ ProductElement.querySelectorAll( 'Accounts ThumbnailsRow' ).forEach( A =>
{ if( V.dataset.variant != A.dataset.variant )
{ return false }
A.querySelector( 'select' ).value = A.dataset.variant
A.querySelectorAll( 'Thumbnail' ).forEach( T => A.removeChild( T ) )
V.querySelectorAll( 'Thumbnail' ).forEach( T =>
{ let newT = A.appendChild( T.cloneNode( true ))
AddClassFor600ms( newT, 'updated' )
})
})
})
Window_onScroll( )
}
const openNewVariantDialog = e =>
{ let NewVariantDialog = e.target.parentNode.parentNode.parentNode
NewVariantDialog.querySelector( 'ConfirmationDialog' )
.classList.toggle( 'hidden' )
NewVariantDialog.querySelector( 'button' )
.classList.toggle( 'hidden' )
let VariantName =
NewVariantDialog.querySelector( `input[name='variant_name']` )
VariantName.value = Now( )
VariantName.select( )
VariantName.focus( )
}
const abortNewVariant = e =>
{ let NewVariantDialog = e.target.parentNode.parentNode.parentNode
NewVariantDialog.querySelector( 'ConfirmationDialog' )
.classList.toggle( 'hidden' )
NewVariantDialog.querySelector( 'button' )
.classList.toggle( 'hidden' )
}
const confirmNewVariant = e =>
{ let NewVariantDialog = e.target
&& e.target.parentNode.parentNode.parentNode
|| e.parentNode.parentNode
, P = NewVariantDialog.parentNode.parentNode
, VariantName = NewVariantDialog.querySelector( `input[name='variant_name']` )
NewVariantDialog.querySelector( 'ConfirmationDialog' )
.classList.toggle( 'hidden' )
NewVariantDialog.querySelector( 'button' )
.classList.toggle( 'hidden' )
if( !VariantName.value.trim( ) )
{ VariantName.value = Now( ) }
const SKU = P.dataset.sku
, Variant = VariantName.value
Products[ SKU ].Variants[ Variant ] = { }
Products[ SKU ].Variants[ Variant ][ 10 ] =
Products[ SKU ].Variants[ '_default' ][ 10 ] || ''
let V = P.querySelector( `Variants [data-variant='${Variant}']` )
if( V )
{ ScrollTo( V )
return false
}
request({ api: 'variant'
, method: 'POST'
, query: { SKU, Variant }
})
let newV = createVariant(
{ SKU
, Variant
, Photos: Products[ SKU ].Variants[ Variant ]
} )
newV.querySelectorAll( 'Thumbnail' ).forEach( T =>
{ AddClassFor600ms( T, 'updated' ) } )
ScrollTo( P.querySelector( 'Variants' ).appendChild( newV ))
P.querySelectorAll( 'Accounts select' ).forEach( S =>
{ let O = d.createElement( 'option' )
O.textContent = Variant
S.appendChild( O )
} )
}
const copyVariant = e =>
{ const R = e.target
&& e.target.parentNode.parentNode
|| e.parentNode.parentNode
, P = R.parentNode.parentNode
, SKU = P.dataset.sku
, CopyOf = R.dataset.variant
, Variant = 'Copy ' + Now( )
console.log({R,P, SKU, CopyOf, Variant})
Products[ SKU ].Variants[ Variant ] = Products[ SKU ].Variants[ CopyOf ]
request({ api: 'variant'
, method: 'POST'
, query: { SKU, Variant, CopyOf }
})
let newV = createVariant(
{ SKU
, Variant
, Photos: Products[ SKU ].Variants[ Variant ]
})
newV.querySelectorAll( 'Thumbnail' ).forEach( T =>
{ AddClassFor600ms( T, 'updated' ) } )
ScrollTo( P.querySelector( 'Variants' ).appendChild( newV ))
P.querySelectorAll( 'Accounts select' ).forEach( S =>
{ let O = d.createElement( 'option' )
O.textContent = Variant
S.appendChild( O )
})
}
const toggleProduct = e =>
{ e.preventDefault( )
let P = e.target.parentNode.parentNode.parentNode
, SKU = P.dataset.sku
if( P.classList.contains( 'targeted' ) )
{ collapseAll( )
P.querySelectorAll( 'Thumbnail' )
.forEach( T => T.classList.remove( 'updated' ) )
}
else
{ expandProduct( P ) }
return true
}
const expandProduct = P =>
{ unzoomAll( )
P.classList.remove( 'hidden' )
P.classList.add( 'targeted' )
ScrollTo( P )
const SKU = P.dataset.sku
setURL( '!'+SKU )
if( P.querySelector( 'Accounts ThumbnailsRow' ) )
{ return true }
let Product = Products[ SKU ] || { }
, Variants = []
, NewVariant = P.querySelector( 'NewVariant' )
, ConfirmNewVariant = NewVariant.querySelector( 'ConfirmNewVariant' )
, AbortNewVariant = NewVariant.querySelector( 'AbortNewVariant' )
, NewVariantName = NewVariant.querySelector( 'input[type=text]' )
NewVariant.querySelector( 'button' ).onclick = openNewVariantDialog
ConfirmNewVariant.onclick = confirmNewVariant
AbortNewVariant.onclick = abortNewVariant
NewVariantName.onkeypress = e =>
{ if( e.key == 'Escape' )
{ AbortNewVariant.click( )
return false
}
else if( e.key == 'Enter' )
{ confirmNewVariant( ConfirmNewVariant ) }
}
if( !Product.Variants )
{ Product.Variants = { _default: { } } }
for( const Variant in Product.Variants )
{ Variants.push( Variant )
if( Variant != '_default' )
{ P.querySelector( 'Variants' ).appendChild(
createVariant(
{ SKU
, Variant
, Photos: Product.Variants[Variant]
})
)
}
}
if( !Object.keys( Product.Accounts ).length )
{ Products[ SKU ].Accounts
= Product.Accounts
= Default_Accounts
}
Accounts_Ordered.map( Account =>
P.querySelector( 'Accounts' ).appendChild
( createAccount(
{ Account
, Variant: Product.Accounts[ Account ] || '_default'
, Variants
, ProductElement: P
, SKU
})
)
)
ScrollTo( P )
}
const Window_onScroll = e =>
{ const Window_Height = w.innerHeight
|| d.documentElement.clientHeight
d.querySelectorAll( 'ThumbnailsRow' )
.forEach( ( R, i, a ) =>
{ let Rect = R.getBoundingClientRect( )
if( ( 0 <= ( Rect.top + Rect.height ) )
&& ( Rect.top <= Window_Height )
)
{ // if( ( i == a.length-1 )
// && ( !d.querySelector( '.targeted' ) )
// )
// { showProducts( ) }
if( ( R.querySelector( 'Thumbnail img' ).src
== R.querySelector( 'Thumbnail' ).dataset.src_small
)
|| ( R.querySelector( 'Thumbnail img' ).src
== R.querySelector( 'Thumbnail' ).dataset.src_big
)
)
{ return false }
R.querySelectorAll( 'Thumbnail:not(.zoomed)' )
.forEach( T => T.querySelector( 'img' ).src = T.dataset.src_small )
}
})
}
const Window_onResize = e =>
{ d.getElementById( 'ZoomedStyle' ).innerText =
`.zoomed
{ top: ${ w.innerHeight/2 - 400 }px !important
; left: ${ w.innerWidth/2 - 400 }px !important
; position: fixed
}
`.replace( /\s\s+/g, '\n' )
Window_onScroll( )
}
const Window_onKeyDown = e =>
{ let Zoomed = d.querySelector( 'Thumbnail.zoomed' )
if( Zoomed )
{ if( e.key == 'Escape' )
{ unzoomAll( )
return true
}
if( /^(k|n|ArrowRight)$/i.test( e.key ))
Zoomed.querySelector( 'Next' ).click( )
if( /^(j|p|ArrowLeft)$/i.test( e.key ))
Zoomed.querySelector( 'Prev' ).click( )
}
else
{ if( e.key == 'Escape' )
{ collapseAll( )
unhideAll( )
showProducts( )
}
if( ( e.key == 'R' && ( e.ctrlKey || e.metaKey ) )
|| ( ( e.which || e.keyCode ) == 116 )
)
{ console.log( 'Hard Reload in 3.. 2.. ' )
_HardReload_in_3_2_ = true
}
}
}
const openThumbnailRemovalDialog = e =>
{ unzoomAll( )
let RemovalButton = e.target
, ConfirmationDialog = RemovalButton.querySelector( 'ConfirmationDialog' )
, Icon = RemovalButton.querySelector( 'Icon' )
if( !( ConfirmationDialog && Icon ))
{ return false }
ConfirmationDialog.classList.remove( 'hidden' )
Icon.classList.add( 'hidden' )
// RemovalButton.removeEventListener( 'click', openThumbnailRemovalDialog )
}
const abortThumbnailRemoval = e =>
{ let RemovalDialog = e.target.parentNode.parentNode.parentNode
RemovalDialog.querySelector( 'Icon' ).classList.remove( 'hidden' )
RemovalDialog.querySelector( 'ConfirmationDialog' ).classList.add( 'hidden' )
// RemovalDialog.addEventListener( 'click', openThumbnailRemovalDialog )
}
const confirmThumbnailRemoval = e =>
{ unzoomAll( )
let RemoveButton = e.target.parentNode.parentNode
, T = RemoveButton.parentNode
, R = T.parentNode
, P = R.parentNode.parentNode
const SKU = P.dataset.sku
, Variant = R.dataset.variant
, Nr = T.dataset.nr
, Hash = T.dataset.hash
console.log( 'removing photo:', SKU, Variant, Nr, Hash )
request({ api: ''
, method: 'DELETE'
, query: { SKU, Variant, Nr }
})
if( 10 == Nr )
{ delete Products[ SKU ].Variants[ Variant ][ 10 ] }
else
{ const newAmountOfRegularPhotos = Object
.keys( Products[ SKU ].Variants[ Variant ] )
.filter( k => 10 > k )
.length - 1
for( let _Nr = Nr; _Nr <= 9; _Nr++ )
{ if( _Nr <= newAmountOfRegularPhotos )
{ Products[ SKU ].Variants[ Variant ][ _Nr ] =
Products[ SKU ].Variants[ Variant ][ Number( _Nr ) + 1 ]
}
else
{ delete Products[ SKU ].Variants[ Variant ][ _Nr ] }
}
}
let V = createVariant(
{ SKU
, Variant
, Photos: Products[ SKU ].Variants[ Variant ]
})
R.parentNode.replaceChild( V, R )
AddClassFor600ms( V, 'changed_numeration' )
updateAccounts( P )
}
const openVariantRemovalDialog = e =>
{ unzoomAll( )
let RemovalButton = e.target
, ConfirmationDialog = RemovalButton.querySelector( 'ConfirmationDialog' )
, Icon = RemovalButton.querySelector( 'Icon' )
if( !( ConfirmationDialog && Icon ))
{ return false }
ConfirmationDialog.classList.remove( 'hidden' )
Icon.classList.add( 'hidden' )
// RemovalButton.removeEventListener( 'click', openVariantRemovalDialog )
}
const abortVariantRemoval = e =>
{ let RemovalDialog = e.target.parentNode.parentNode
RemovalDialog.querySelector( 'icon' ).classList.remove( 'hidden' )
RemovalDialog.querySelector( 'ConfirmationDialog' ).classList.add( 'hidden' )
// RemovalDialog.addEventListener( 'click', openVariantRemovalDialog )
}
const confirmVariantRemoval = e =>
{ const R = e.target.parentNode.parentNode.parentNode.parentNode
, P = R.parentNode.parentNode
, Variant = R.dataset.variant
, SKU = P.dataset.sku
console.log( 'removing', SKU, Variant )
request({ api: 'variant'
, method: 'DELETE'
, query: { SKU, Variant }
})
if( Products[ SKU ].Variants[ Variant ] )
{ delete Products[ SKU ].Variants[ Variant ]
localStorage.setItem( SKU, JSON.stringify( Products[ SKU ] ) )
}
// if( Variant == '_default' )
// let chkNewDefault = P.querySelector('input[type=checkbox]:not(:checked)')
// if( chkNewDefault )
// changeDefaultVariant( chkNewDefault )
// }
// }
R.parentNode.removeChild( R )
}
const unzoomAll = ( ) =>
{ d.querySelectorAll( 'Thumbnail.zoomed' ).forEach( z =>
{ //console.log(z)
z.querySelector( 'Prev' ).classList.add( 'hidden' )
z.querySelector( 'Next' ).classList.add( 'hidden' )
z.querySelector( 'Zoom i' ).classList.add( 'fa-search-plus' )
z.querySelector( 'Zoom i' ).classList.remove( 'fa-search-minus' )
z.classList.remove( 'zoomed' )
z.querySelector( 'img' ).src = z.dataset.src_small
})
}
const ThumbnailZoomToggle = T =>
{ if( T.classList.contains( 'zoomed' ) )
{ unzoomAll( ) }
else
{ ThumbnailZoomIn( T ) }
}
const ThumbnailZoomIn = T =>
{ unzoomAll( )
T.querySelector( 'Prev' ).classList.remove( 'hidden' )
T.querySelector( 'Next' ).classList.remove( 'hidden' )
T.querySelector( 'Zoom i' ).classList.remove( 'fa-search-plus' )
T.querySelector( 'Zoom i' ).classList.add( 'fa-search-minus' )
T.classList.add( 'zoomed' )
T.querySelector( 'img' ).src = T.dataset.src_big
}
const Moving_inProgress = Pos =>
{ Moving_Thumbnail.style.top = Pos.pageY+10 + 'px'
Moving_Thumbnail.style.left = Pos.pageX+5 + 'px'
Pos.preventDefault( )
}
const Moving_Stop = e =>
{ e.preventDefault( )
d.removeEventListener( 'mousemove', Moving_inProgress )
d.removeEventListener( 'mouseup', Moving_Stop )
if( Moving_Thumbnail != d.querySelector( 'Thumbnail.moving' ) )
{ return false }
let R = Moving_Thumbnail.parentNode
, P = R.parentNode.parentNode
, SKU = P.dataset.sku
, Variant = R.dataset.variant
, OldNr = Moving_Thumbnail.dataset.nr
R.querySelectorAll( 'MovePlaceholder' )
.forEach( p => p.classList.add( 'hidden' ) )
R.classList.remove( 'has_moving' )
Moving_Thumbnail.classList.remove( 'moving' )
Moving_Thumbnail.style = ''
let Place = d.querySelector( 'Thumbnail MovePlaceholder:hover' )
if( !Place
|| ( Place.parentNode.dataset.uuid
== Moving_Thumbnail.dataset.prevuuid
)
)
{ return false }
// rearrange photos
let NewNr = Number( Place.textContent )
console.log( 'reordering photos', OldNr, '>>', NewNr )
Place.parentNode.insertAdjacentElement( 'afterend', Moving_Thumbnail )
AddClassFor600ms( Moving_Thumbnail.querySelector( 'img' ), 'updated' )
R.querySelectorAll( 'Thumbnail' )
.forEach( ( T, i, a ) =>
{ const NrBefore = T.dataset.nr
, NrAfter = ( i<10 ? ( i==0 ? 10 : i ) : i+1 )
T.dataset.nr
= T.querySelector( 'ThumbnailCaption' ).textContent
= NrAfter
T.dataset.uuid
= T.title
= T.dataset.sku + ':' + NrAfter
T.dataset.prevuuid = T.dataset.sku + ':' + ( [ a.length, 10, ...range( 1,10 ) ][i] || i )
T.dataset.nextuuid = T.dataset.sku + ':' + ( i==a.length-1 ? 10 : ( i<9 ? i+1 : i+2 ) )
})
AddClassFor600ms( R, 'changed_numeration' )
request({ api: 'renumber'
, method: 'PUT'
, query: { SKU, Variant, OldNr, NewNr }
})
updateAccounts( P )
Moving_Thumbnail = null
}
const Move = e =>
{ if( e.button == 0 )
{ Moving_Thumbnail = e.target.parentNode
Moving_Thumbnail.classList.add( 'moving' )
Moving_Thumbnail.parentNode.classList.add( 'has_moving' )
let R = Moving_Thumbnail.parentNode
R.querySelectorAll( 'Thumbnail:not( .moving ) MovePlaceholder' )
.forEach( ( MovePlaceholder, i, a ) =>
{ MovePlaceholder.textContent = i+1
MovePlaceholder.classList.remove( 'hidden' )
} )
Moving_inProgress( e )
d.addEventListener( 'mousemove', Moving_inProgress )
d.addEventListener( 'mouseup', Moving_Stop )
e.preventDefault( )
}
}
const Prev = e =>
{ const T = e.target.parentNode
, R = T.parentNode
, PrevT = R.querySelector( `[data-uuid='${T.dataset.prevuuid}']` )
if( PrevT )
{ ThumbnailZoomIn( PrevT ) }
}
const Next = e =>
{ const T = e.target.parentNode
, R = T.parentNode
, NextT = R.querySelector( `[data-uuid='${T.dataset.nextuuid}']` )
if( NextT )
{ ThumbnailZoomIn( NextT ) }
}
const createThumbnail = ({ SKU, Nr, Hash, PrevUUID, NextUUID, Variant }) =>
{ let T = ThumbnailTemplate
.querySelector( 'Thumbnail' )
.cloneNode( true )
Hash = Hash || ''
T.dataset.hash = Hash
T.dataset.src_big = getPhotoURL({ SKU, Hash, Nr, Size:'M' })
T.dataset.src_small = getPhotoURL({ SKU, Hash, Nr, Size:'S' }) || '/placeholder.svg'
T.querySelector( 'img' ).src = '/placeholder.svg'
T.querySelector( 'ThumbnailCaption' ).textContent = Nr
T.querySelector( 'Upload input' ).onchange = UploadSinglePhoto
T.querySelector( 'Remove' ).addEventListener( 'click', openThumbnailRemovalDialog )
T.querySelector( 'Remove AbortRemoval' ).onclick = abortThumbnailRemoval
T.querySelector( 'Remove ConfirmRemoval' ).onclick = confirmThumbnailRemoval
T.querySelector( 'Move' ).onmousedown = Move
T.querySelector( 'Zoom' ).onclick = e => ThumbnailZoomToggle( e.target.parentNode )
T.querySelector( 'Prev' ).onclick = Prev
T.querySelector( 'Next' ).onclick = Next
T.dataset.prevuuid = PrevUUID
T.dataset.nextuuid = NextUUID
T.dataset.sku = SKU
T.dataset.nr = Nr
T.dataset.uuid
= T.title
= SKU+':'+Nr
return T
}
const changeDefaultVariant = e =>
{ const newDefaultCheckbox = e.target || e
, R = newDefaultCheckbox.parentNode.parentNode
, P = R.parentNode.parentNode
, SKU = P.dataset.sku
, NewDefaultVariant = R.dataset.variant
, oldDefaultRow = P.querySelector( `Variants ThumbnailsRow[data-variant='_default']` )
, oldDefaultCheckbox = oldDefaultRow.querySelector( `input[type='checkbox']` )
, oldVarinatRenamedTo = Now( )
oldDefaultRow.dataset.variant = oldVarinatRenamedTo
oldDefaultCheckbox.checked
= oldDefaultCheckbox.disabled
= false
newDefaultCheckbox.checked
= newDefaultCheckbox.disabled
= true
oldDefaultRow.querySelector('RowTitle').textContent = oldVarinatRenamedTo
Products[ SKU ].Variants[ oldVarinatRenamedTo ] =
Products[ SKU ].Variants[ '_default' ]
R.dataset.variant = '_default'
R.querySelector('RowTitle').textContent = ''
Products[ SKU ].Variants[ '_default' ] =
Products[ SKU ].Variants[ NewDefaultVariant ]
delete Products[ SKU ].Variants[ NewDefaultVariant ]
updateAccounts( P )
request({ api: 'setDefaultVariant'
, method: 'PUT'
, query: { SKU, Variant: NewDefaultVariant, oldVarinatRenamedTo }
})
AddClassFor600ms( R, 'updated' )
console.log( `set '${NewDefaultVariant}' as default of '${SKU}',`
, 'old default renamed to', oldVarinatRenamedTo
)
}
const createVariant = ({ SKU, Variant, Photos }) =>
{ let R = ThumbnailsRowTemplate
.querySelector( 'ThumbnailsRow' )
.cloneNode( true )
let isDefault = R.querySelector( 'RowDescription input[type=checkbox]' )
isDefault.onchange = changeDefaultVariant
isDefault.checked = ( Variant == '_default' )
isDefault.disabled = isDefault.checked
R.querySelector( 'RowTitle' ).textContent = Variant.replace( '_default', '' )
R.dataset.variant = Variant
R.querySelector( 'input[type=file]' ).onchange = UploadMultiplePhotos
R.querySelector( 'RowDescription Remove' ).addEventListener( 'click', openVariantRemovalDialog )
R.querySelector( 'RowDescription Remove AbortRemoval' ).onclick = abortVariantRemoval
R.querySelector( 'RowDescription Remove ConfirmRemoval' ).onclick = confirmVariantRemoval
R.querySelector( 'RowDescription Copy' ).onclick = copyVariant
const Photo_Set = [ ...( new Set([ '10', ...Object.keys( Photos ).sort( ) ]) ) ]
Photo_Set.forEach( ( Nr, i, a ) =>
{ R.appendChild( createThumbnail(
{ SKU
, Nr
, Hash: Photos[Nr] || ''
, Variant
, PrevUUID: SKU + ':' + ( a[i-1] ? a[i-1] : a[a.length-1] )
, NextUUID: SKU + ':' + ( a[i+1] ? a[i+1] : a[0] )
} ) )
})
return R
}
const createAccount = ({ Account, Variant, Variants, ProductElement, SKU }) =>
{ const VariantRowSelector = `Variants ThumbnailsRow[data-variant='${Variant}']`
, DefaultRowSelector = `Variants ThumbnailsRow`
let R = ( ProductElement.querySelector( VariantRowSelector )
|| ProductElement.querySelector( DefaultRowSelector )
).cloneNode( true )
R.querySelector( 'RowDescription input[type=checkbox]' )
.classList.add( 'hidden' )
R.dataset.account
= R.querySelector( 'RowTitle' ).textContent
= Account
let S = R.querySelector( `select[name='VariantSelector']` )
S.onchange = VariantSelector_onChange
Variants.forEach( V =>
{ let O = d.createElement( 'option' )
O.textContent = V
O.selected = ( V == Variant )
S.appendChild( O )
} )
return R
}
const createProduct = Product =>
{ if( d.querySelector( `ProductElement[id='${Product.SKU}']` ) )
{ return false }
let P = ProductTemplate.querySelector( 'ProductElement' )
.cloneNode( true )
P.dataset.searchindex = getSearchIndex( Product.SKU, Product.Title, Product.Type )
P.querySelector( 'ProductAmount' ).textContent = Product.Amount
P.querySelector( 'ProductSKU a' ).onclick = toggleProduct
P.querySelector( 'ProductSKU a' ).href = '/photos/!'+Product.SKU
P.querySelector( 'ProductSKU a' ).textContent
= P.dataset.sku
= P.id
= Product.SKU
P.querySelector( 'ProductTitle' ).innerHTML =
Product.Title.replace( /\[/, ' <hr>[' )
const Variant = '_default'
P.querySelector( 'Variants' ).appendChild( createVariant(
{ SKU: Product.SKU
, Variant
, Photos: Product.Variants[ Variant ]
}))
return P
}
const ShowSKUs = Desired =>
new Promise( (resolve, reject) =>
{ setProgressBarTotal( Desired.length ) // Desired.length >= MaxChunk
// && MaxChunk
// || Desired.length
Desired.forEach( ( P, i, a ) =>
{ setTimeout( ( )=>
{ unloadedProducts.splice( unloadedProducts.indexOf( P ), 1 )
incrementProgressBar( )
const newP = Main.appendChild( createProduct( Products[P] ) )
// if( i==0 )
// ScrollTo( newP )
// }
if( i == 10 )
{ Window_onScroll( ) }
if( i==a.length-1 )
{ resolve( true ) }
}, 1 )
})
}).catch( err => console.error( 'cant [ShowSKUs]', err ) )
const onSearch = async e =>
{ if( e ) e.preventDefault( )
searchField.value = searchField.value.trim( )
chkShowOnlyAvaliable.checked = false
UpdateCounter('')
setURL( searchField.value )
if( searchField.value === '*' )
{ // show all
unzoomAll( )
collapseAll( )
unhideAll( )
showProducts( 1e10 )
return true
}
if( searchField.value[0] === '-' )
{ // show w/o photos
unzoomAll( )
collapseAll( )
unhideAll( )
const AbsentPhoto = Number( searchField.value
.replace(/^\-/, '')
.trim( )
|| 1
)
let Desired = []
for( const SKU in Products )
{ const P = Products[ SKU ]
if( !P.Variants[ '_default' ][ AbsentPhoto ] )
{ Desired.push( SKU ) }
}
d.querySelectorAll( 'ProductElement' )
.forEach( P =>
{ if( !Desired.includes( P.dataset.sku ) )
{ P.classList.add( 'hidden' ) }
})
await ShowSKUs( Desired )
d.querySelectorAll( 'ProductElement' )
.forEach( P =>
{ if( !Desired.includes( P.dataset.sku ) )
{ P.classList.add( 'hidden' ) }
})
UpdateCounter( )
return true
}
const SearchString = getSearchIndex( searchField.value )
if( SearchString )
{ const Desired = unloadedProducts.filter( P =>
getSearchIndex( P, Products[P].Title, Products[P].Type )
.includes( SearchString )
)
if( Desired.length )
{ await ShowSKUs( Desired ) }
showOnlySearched({ expandSoloProduct: true })
UpdateCounter( )
}
else
{ unhideAll( )
collapseAll( )
UpdateCounter( )
}
}
const showProducts = ( Amount = MaxChunk ) =>
{ setProgressBarTotal
( ( unloadedProducts.length >= Amount )
&& Amount
|| unloadedProducts.length
)
unloadedProducts.every( ( SKU, i, a ) =>
{ let P = createProduct( Products[ SKU ] )
if( P )
{ setTimeout( ( )=>
{ incrementProgressBar( )
Main.appendChild( P )
if( i>=Amount-1 || i==a.length-1 )
{ Window_onScroll( ) }
}, 1 )
}
if( i>=Amount-1 || i==a.length-1 )
{ unloadedProducts.splice( 0, Amount )
return false
}
return true
} )
}
const showOnlySearched = ({ expandSoloProduct }) =>
{ searchField.value = searchField.value.trim( )
const SearchString = getSearchIndex( searchField.value )
, SKU = searchField.value.replace( /^[!\?\*]+/, '' )
, ForceSKU = ( searchField.value[0] == '!' )
if( ForceSKU && Products[ SKU ] )
{ d.querySelectorAll( `ProductElement:not([data-sku='${ SKU }'])` )
.forEach( P => P.classList.add( 'hidden' ) )
let ShownProduct = d.querySelector( `ProductElement[data-sku='${ SKU }']` )
ShownProduct.classList.remove( 'hidden' )
expandProduct( ShownProduct )
return true
}
if( !SearchString )
{ unhideAll( )
Window_onScroll( )
return false
}
d.querySelectorAll( `ProductElement:not([data-searchindex*='${SearchString}'])` )
.forEach( P => P.classList.add( 'hidden' ) )
let ShownProducts =
d.querySelectorAll( `ProductElement[data-searchindex*='${SearchString}']` )
ShownProducts.forEach( P => P.classList.remove( 'hidden' ) )
if( expandSoloProduct && ShownProducts.length==1 )
{ expandProduct( ShownProducts[0] ) }
else if( ShownProducts.length > 1 )
{ collapseAll( )
ScrollTo( ShownProducts[0] )
}
Window_onScroll( )
}
const loadData = ( ) =>
new Promise( async ( resolve, reject ) =>
{ BaseURL = await request({ api: 'baseURL' })
localStorage.setItem( 'BaseURL', BaseURL )
console.log( 'BaseURL:', BaseURL )
Accounts_Ordered = ( await request({ api: 'accounts' }) )
.split( /[\r\n]+/ )
localStorage.setItem( 'Accounts_Ordered', JSON.stringify( Accounts_Ordered ) )
console.log( 'Accounts:', JSON.stringify( Accounts_Ordered, false, 2 ) )
const Default_Accounts = Object.assign
( { }, ...Accounts_Ordered.map( A => ({[ A ] : '_default' }) ) )
localStorage.setItem( 'Default_Accounts', JSON.stringify( Default_Accounts, false, 2 ) )
let AllPhotos = []
try
{ console.log( 'getting Photos...' )
AllPhotos = JSON.parse( await request(
{ api: ''
, query: { since: localStorage.getItem( 'lastUpdated' ) || 0 }
}) )
console.log
( 'new rows in DB.Photos_Ordered ( since'
, localStorage.getItem( 'lastUpdated' ) || 0
,'):'
, AllPhotos.length
)
}
catch( err )
{ console.error( err )
Products = { 'ERR-01-001': Product_Error }
reject( false )
return false
}
localStorage.setItem( 'lastUpdated', Now( ) )
console.log( 'lastUpdated:', localStorage.getItem( 'lastUpdated' ) )
AllPhotos.forEach( P =>
{ const { SKU, Variant } = P
if( !Products[ SKU ] )
{ Products[ SKU ] =
{ SKU
, Title: P.Title || ''
, Type: P.Type || ''
, Amount: P.Amount || 0
, id: P.id
, Variants: { '_default': { } }
, Accounts: { }
}
if( Products[ SKU ].Type.trim( )
&& !ProductTypes.has( Products[ SKU ].Type.trim( ) )
)
{ ProductTypes.add( Products[ SKU ].Type.trim( ) ) }
}
if( P.Accounts )
{ P.Accounts
.split(/[\r\n]+/)
.filter( A => A && A.trim( ) && A != 'undefined' )
.forEach( Account =>
{ Products[ SKU ].Accounts[ Account ] = Variant })
}
if( Variant )
{ if( !Products[ SKU ].Variants[ Variant ] )
{ Products[ SKU ].Variants[ Variant ] = { } }
for( let Nr = 1; Nr < 11; Nr++ )
{ if( P[ Nr ] )
{ Products[ SKU ].Variants[ Variant ][ Nr ]
= getHashFromPhotoURL( P[ Nr ] )
}
}
}
})
for( let SKU in Products )
{ //console.log( SKU, Products[ SKU ] )
if( SKU )
{ localStorage.setItem
( SKU, JSON.stringify( Products[ SKU ] ) )
}
}
resolve( true )
}).catch( err => console.error( 'cant [loadData]', err ) )
const init = async ( ) =>
{ w.onkeydown = Window_onKeyDown
w.onscroll = Window_onScroll
w.onresize = Window_onResize
Window_onResize( )
searchForm.onsubmit = onSearch
searchField.focus( )
// searchField.onchange = Search_onKeypress
// searchField.onkeypress = Search_onKeypress
btnShowAll.onclick = e =>
{ if( e ) e.preventDefault( )
searchField.value = '*'
e.target.blur( )
onSearch( )
return false
}
btnShowWithoutPhotos.onclick = e =>
{ if( e ) e.preventDefault( )
searchField.value = '-1'
e.target.blur( )
onSearch( )
return false
}
chkShowOnlyAvaliable.onchange = ShowOnlyAvaliable
ProductTemplate.innerHTML =
d.getElementById( 'ProductTemplate' ).innerHTML
ThumbnailsRowTemplate.innerHTML =
d.getElementById( 'ThumbnailsRowTemplate' ).innerHTML
ThumbnailTemplate.innerHTML =
d.getElementById( 'ThumbnailTemplate' ).innerHTML
if( !localStorage.getItem( 'lastUpdated' )
|| !Object.keys( Products ).length
)
{ await loadData( ) }
else
{ loadData( ) }
PopulateSearchWithTypes( )
unloadedProducts = Object.keys( Products )
unloadedProducts.sort( (a,b) => Products[a].id - Products[b].id )
// console.log( 'active Products:', unloadedProducts )
let locHash = unURIHash( )
if( w.location.search || locHash )
{ let u = new URLSearchParams( w.location.search )
MaxChunk = Number( u.get( 'max' ) || 100 )
let s = locHash
searchField.value = s.toUpperCase( )
onSearch( )
}
else
{ showProducts( MaxChunk ) }
}
w.addEventListener( 'DOMContentLoaded', init( ), true )
w.addEventListener( 'beforeunload', e =>
{
if( _HardReload_in_3_2_ )
{ console.warn( 'CLEAR!!' )
localStorage.clear( )
}
})