Manage ASL magazines and their articles.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
asl-articles/web/src/utils.js

280 lines
9.2 KiB

import React from "react" ;
import ReactDOMServer from "react-dom/server" ;
import { gAppRef } from "./App.js" ;
const isEqual = require( "lodash.isequal" ) ;
// --------------------------------------------------------------------
export function checkConstraints( required, requiredCaption, optional, optionalCaption, accept ) {
// check if constraints have been disabled (for testing porpoises only)
if ( gAppRef.isDisableConstraints() ) {
accept() ;
return ;
}
// check the required constraints
let msgs=[], setFocusTo=null ;
if ( required ) {
for ( let constraint of required ) {
if ( constraint[0]() ) {
msgs.push( constraint[1] ) ;
if ( constraint[2] && !setFocusTo )
setFocusTo = constraint[2] ;
}
}
}
if ( msgs.length > 0 ) {
gAppRef.showErrorMsg(
makeSmartBulletList( requiredCaption, msgs, "constraint" ),
setFocusTo
) ;
return ;
}
// check the optional constraints
if ( optional ) {
for ( let constraint of optional ) {
if ( constraint[0]() ) {
msgs.push( constraint[1] ) ;
if ( constraint[2] && !setFocusTo )
setFocusTo = constraint[2] ;
}
}
}
if ( msgs.length > 0 ) {
// some constraints failed - ask the user if they want to continue
let content = <div style={{float:"left"}}> { makeSmartBulletList( optionalCaption, msgs, "constraint" ) } </div> ;
gAppRef.ask( content, "ask", {
OK: () => { accept() },
Cancel: null
}, setFocusTo ) ;
return ;
}
// everything passed - accept the values
accept() ;
}
export function confirmDiscardChanges( oldVals, newVals, accept ) {
// check if confirmations have been disabled (for testing porpoises only)
if ( gAppRef.isDisableConfirmDiscardChanges() ) {
accept() ;
return ;
}
// check if the values have changed
if ( isEqual( oldVals, newVals ) ) {
// nope - just do it
accept() ;
} else {
// yup - ask the user to confirm first
gAppRef.ask( "Do you want to discard your changes?", "ask", {
OK: accept,
Cancel: null,
} ) ;
}
}
// --------------------------------------------------------------------
export function sortSelectableOptions( options ) {
options.sort( (lhs,rhs) => {
lhs = ReactDOMServer.renderToStaticMarkup( lhs.label ) ;
rhs = ReactDOMServer.renderToStaticMarkup( rhs.label ) ;
return lhs.localeCompare( rhs ) ;
} ) ;
}
export function unloadCreatableSelect( sel ) {
// unload the values from a CreatableSelect
if ( ! sel.state.value )
return [] ;
const vals = sel.state.value ;
// dedupe the values (trying to preserve order)
let vals2=[], used={} ;
vals.forEach( val => {
if ( ! used[ val.label ] ) {
vals2.push( val ) ;
used[ val.label ] = true ;
}
} ) ;
return vals2 ;
}
export function makeTagLists( tags ) {
// convert the tags into a list suitable for CreatableSelect
// NOTE: react-select uses the "value" field to determine which choices have already been selected
// and thus should not be shown in the droplist of available choices.
let tagList = [] ;
if ( tags ) {
tags.map(
(tag) => tagList.push( { value: tag, label: tag } )
) ;
}
// create another list for all known tags
let allTags = gAppRef.dataCache.data.tags.map(
(tag) => { return { value: tag[0], label: tag[0] } }
) ;
return [ tagList, allTags ] ;
}
// --------------------------------------------------------------------
// NOTE: The format of a scenario display name is "SCENARIO NAME [SCENARIO ID]".
export function makeScenarioDisplayName( scenario ) {
let scenario_display_id, scenario_name
if ( Array.isArray( scenario ) ) {
// we've been given a scenario ID/name
scenario_display_id = scenario[0] ;
scenario_name = scenario[1] ;
} else {
// we've been given a scenario object (dict)
scenario_display_id = scenario.scenario_display_id ;
scenario_name = scenario.scenario_name ;
}
if ( scenario_name && scenario_display_id )
return scenario_name + " [" + scenario_display_id + "]" ;
else if ( scenario_name )
return scenario_name ;
else if ( scenario_display_id )
return scenario_display_id ;
else
return "???" ;
}
export function parseScenarioDisplayName( displayName ) {
// try to locate the scenario ID
displayName = displayName.trim() ;
let scenarioId=null, scenarioName=displayName ;
if ( displayName[ displayName.length-1 ] === "]" ) {
let pos = displayName.lastIndexOf( "[" ) ;
if ( pos !== -1 ) {
// found it - separate it from the scenario name
scenarioId = displayName.substr( pos+1, displayName.length-pos-2 ).trim() ;
scenarioName = displayName.substr( 0, pos ).trim() ;
}
}
return [ scenarioId, scenarioName ] ;
}
// --------------------------------------------------------------------
export function updateRecord( rec, newVals ) {
// update a record with new values
for ( let key in newVals )
rec[ key ] = newVals[ key ] ;
}
export function makeCollapsibleList( caption, vals, maxItems, style, listKey ) {
if ( ! vals || vals.length === 0 )
return null ;
let items=[], excessItems=[] ;
let excessItemsRef=null, excessItemsMoreRef=null, flipButtonRef=null ;
for ( let i=0 ; i < vals.length ; ++i ) {
let item ;
if ( typeof vals[i] === "string" )
item = <li key={i} dangerouslySetInnerHTML={{ __html: vals[i] }} /> ;
else
item = <li key={i}> {vals[i]} </li> ; // nb: we assume we were given JSX
( i < maxItems ? items : excessItems ).push( item ) ;
}
function flipExcessItems() {
const pos = flipButtonRef.src.lastIndexOf( "/" ) ;
const show = flipButtonRef.src.substr( pos ) === "/collapsible-down.png" ;
excessItemsRef.style.display = show ? "block" : "none" ;
flipButtonRef.src = show ? "/images/collapsible-up.png" : "/images/collapsible-down.png" ;
excessItemsMoreRef.style.display = show ? "none" : "block" ;
}
if ( excessItems.length === 0 )
caption = <span> {caption}: </span> ;
else
caption = <span> {caption} <span className="count"> ({vals.length}) </span> </span> ;
let onClick, style2 ;
if ( excessItems.length > 0 ) {
onClick = flipExcessItems ;
style2 = { cursor: "pointer" } ;
}
return ( <div className="collapsible" style={style} key={listKey}>
<div className="caption" onClick={onClick} style={style2} >
{caption}
{ excessItems.length > 0 && <img src="/images/collapsible-down.png" ref={r => flipButtonRef=r} alt="Show/hide extra items." /> }
</div>
<ul> {items} </ul>
{ excessItems.length > 0 && <div>
<ul className="excess" ref={r => excessItemsRef=r} style={{display:"none"}}>
{excessItems}
</ul>
<div className="more" onClick={onClick} ref={r => excessItemsMoreRef=r}> more... </div>
</div> }
</div> ) ;
}
export function makeCommaList( vals ) {
let result = [] ;
if ( vals ) {
for ( let i=0 ; i < vals.length ; ++i ) {
result.push( vals[i] ) ;
if ( i < vals.length-1 )
result.push( ", " ) ;
}
}
return result ;
}
export function makeSmartBulletList( caption, vals, className ) {
caption = <div className="caption"> {caption} </div> ;
if ( !vals || vals.length === 0 )
return caption ;
else if ( vals.length === 1 )
return <div> {caption} <p className={className}> {vals[0]} </p> </div> ;
else {
let bullets = vals.map( (v,i) => <li key={i} className={className}> {v} </li> ) ;
return <div> {caption} <ul> {bullets} </ul> </div> ;
}
}
export function bytesDisplayString( nBytes )
{
if ( nBytes === 1 )
return "1 byte" ;
var vals = [ "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" ] ;
for ( let i=1 ; i < vals.length ; i++ ) {
if ( nBytes < Math.pow( 1024, i ) )
return ( Math.round( ( nBytes / Math.pow(1024,i-1) ) * 100 ) / 100 ) + " " + vals[i-1] ;
}
return nBytes ;
}
export function slugify( val ) {
return val.toLowerCase().replace( " ", "-" ) ;
}
export function pluralString( n, str1, str2 ) {
if ( n === 1 )
return n + " " + str1 ;
else
return n + " " + (str2 ? str2 : str1+"s") ;
}
export function ciCompare( lhs, rhs ) {
return lhs.localeCompare( rhs, undefined, { sensitivity: "base" } ) ;
}
export function isNumeric( val ) {
if ( val === null || val === undefined )
return false ;
val = val.trim() ;
if ( val === "" )
return false ;
return ! isNaN( val ) ;
}
export function isLink( val ) {
if ( val.substr(0,7) === "http://" || val.substr(0,8) === "https://" )
return true ;
if ( val.substr(0,7) === "file://" )
return true ;
return false ;
}