12/19/2024
Screen shot:
View working sample app at:
https://mobiwek61.github.io/amhistory/aLincoln1
on mobile device. QR code:
Github repository of working sample source using this package
more complex examples are here also.
https://github.com/mobiwek61/mobiwek61menu/tree/main/mobiwek61menu_demo/simple-demo
Github repository of source of this package may be out of date relative to npmjs bundle:
https://github.com/mobiwek61/mobiwek61menu
Email of author of this package and demos:
mobiwek61@gmail.com
Pronounced "mobiweka": "mobile" plus "Weka bird" (for no particular reason). "a" as hexadecimal 61 to eliminate possible name clashes with other entities.
A project to investigate ways of fitting a big menu into a small mobile screen.
- Initial goal is to present zoomable images of art and written historical text. Zoom action is independent of menu icon in upper left and info button at bottom.
- The menu can have many levels and extends far beyond the mobile device's physical screen width, where screen is a draggable viewport.
- Menu retains state between navigations, includes fixed location home button and font size adjustment.
- Menu is translucent and floats above current content with no background.
- Menu provisioned using JSON. To facilitate transform from other data formats, a JSON schema provided in this document. Tested OK using AI (Copilot) to populate menu. See description at end.
Below are the 2 files which determine menu appearance and actions:
- The menu structure in below example is defined by DevMenuSpec.js
- The menu actions and Routes is defined in ReactAppForDevelopment.js
See above repositories for full example code.
DevMenuSpec.js
import React from 'react';
const space4=<> </> // this is JSX. Must be bounded by <> and </>
/** example of creating a reusable menu leaf as a const */
const const_winslowHomerFogWarning = { LEAF:"fog Warning", mwmkey: "winslowHomerFogWarning",
txtdesc:<>The fisherman is worried he cannot return to the schooner because of impending fog.<br/>
If he can't get back, he will be lost at sea.<br/>
From the direction of the swells, the wind and waves are against him.</>,
mwmtype: "image",
imgurl:'/jpeg/WIKIMEDIA-Winslow-HomerFogWarning.jpg'
};
const mobiwekMenuJSONexample_1 =
{ REV:'simple example',
// schema wont work here because not a JSON file.. "$schema": "./TestJsonASchema.json", // visual studio code way of custom schema
DD_MENU_HEAD:[
{ LEAF:"About", mwmkey: "about_app", mwmtype:"mwtypNull" },
{ BRANCH:"American History", mwmkey:"amhistory", items: [
{ LEAF:"Abraham Lincoln Photo", mwmkey: "aLincoln1",
mwmtype: "image",
txtdesc:<>Anthony, Edward and Brady, Mathew B. 1860 https://www.loc.gov/item/2021669449/<br/></>,
gotfrom:'https://tile.loc.gov/image-services/iiif/service:gdc:gdcwdl:wd:l_:01:50:2:wdl_01502:icon966301/full/pct:50/0/default.jpg',
imgurl:'/jpeg/LOC-AbeLincoln.jpg'
},
{ BRANCH:"aircraft", mwmkey:"aircraft", items:[
{ LEAF:<>space shuttle<br/>{space4}Introduction</>, mwmkey:"spaceShuttleIntro", mwmtype:"reactRoute",
clickMeLink:"spaceShuttleTransport", clickMeLink2:"spaceShuttleOrbit" },
{ LEAF:<>space shuttle<br/>{space4}transport</>, mwmkey:"spaceShuttleTransport", mwmtype:"image",
txtdesc:<>from https://upload.wikimedia.org/wikipedia/commons/thumb/3/35/Shuttle_Carrier_Aircraft_transporting_Discovery_departs_Kennedy_Space_Center.jpg<br/></>,
imgurl:'/jpeg/hist/aerospace/WIKIMEDIA-spaceShuttleTransport.jpg' },
{ LEAF:<>space shuttle<br/>{space4}orbit</>, mwmkey:"spaceShuttleOrbit", mwmtype:"image",
txtdesc:<>from https://commons.wikimedia.org/wiki/File:STS007-32-1702.jpg<br/>
This file is in the public domain in the United States because it was solely created by NASA.
<br/>Image is unenhanced, original Kodachrome I guess. Shows just how old it is.</>,
imgurl:'/jpeg/hist/aerospace/WIKIMEDIA-spaceShuttleOrbit.jpg' }
]},
{ BRANCH:"Ben Franklin", mwmkey:"benFranklin", items: [
{ LEAF:"Speed Of Ships", mwmkey: "speedOfShips",
mwmtype: "handwriting",
txtdesc:<>Benjamin Franklin technical notes about ships<br/>Benjamin Franklin Papers: Series III, 1728-1841; Miscellaneous material; 1728-1783 (vol. 31)<br/>from https://www.loc.gov/resource/mss21451.mss21451-012_00093_00422/?sp=114</>,
imgurl:'/jpeg/hist/benFranklin/LOC-benFranklinSpeedOfShips.jpg'
},
{ LEAF:"Ship diagram", mwmkey: "shipDiagram",
mwmtype: "handwriting",
txtdesc:<>diagram of sailing ship and wind direction<br/>Benjamin Franklin Papers: Series III, 1728-1841; Miscellaneous material; 1784-1841 , undated (vol. 32)<br/>from https://www.loc.gov/resource/mss21451.mss21451-012_00423_00602/?sp=3</>,
imgurl:'/jpeg/hist/benFranklin/LOC-benFranklin_sailingShipDiagram.jpg'
},
]},
]}, // END OF BRANCH amhistory
{ BRANCH:"x15 Spaceplane", mwmkey:"x15", items:[
{ LEAF:"x15 Intro", mwmkey:"x15", mwmtype:"mwtypNull", clickMeLink:"x15image" },
{ LEAF:<>picture of<br/>x15</>, mwmkey:"x15image", mwmtype:"image",
txtdesc:<>from https://www.edwards.af.mil/News/Photos/igphoto/2001667598/</>,
imgurl:'/jpeg/hist/aerospace/WIKIMEDIA-X-15_flying.jpg' }
]},
const_winslowHomerFogWarning,
]};
export { mobiwekMenuJSONexample_1 }
ReactAppForDevelopment.js
You add code to this for custom handlers by adding to the below callback function. Attributes added to the JSON menu spec will appear here in the retrieved menu item object.
import React, { useEffect, useState } from 'react';
import { BrowserRouter, Route, Routes, Outlet, useParams, useNavigate, useLocation } from "react-router-dom";
import { mobiwekMenuJSONexample_1 } from "./DevMenuSpec";
import devProjCSS from './devProject.module.css'
import { findJsonObjectByFullPath, validateMwMenu,
MediaPictureWithInfo, MobiWekMenuFrame, MobiWekPageWrapper }
//from '../../../src/PackageTreeEntry'; // coders of the package do it this way. use this when developing the package
from 'mobiwek61menu'; // Users of package do it this way. use this when the package has been "npm installed"'ed.
function ReactAppForDevelopment() {
/* !!! useEffect() !!! is React's lifecycle event, like onLoad(), onRefresh() etc. */
useEffect(() => { console.log('ReactAppForDevelopment useEffect') }, []);
const theMobiwekMenu = mobiwekMenuJSONexample_1
var errorJSX = validateMwMenu(theMobiwekMenu) // not really up to date anymore
if (errorJSX !== null) return(errorJSX)
// get "css variable" from css file using getComputedStyle().
var cssVariableFromCSSfile_fontsizeA =
getComputedStyle(document.body).getPropertyValue('--fontSizeA')
{/* the code below has parts which looks like HTML is actually JSX. One big difference is when css
style is specified, things like margin-bottom become marginBottom (camelcase) and the value
appears in quotes. Also note the double brackets around css. */}
return (
<BrowserRouter>
<Routes>
<Route path='/' // this route always taken. Serves as menu frame for content
// ^ all routes hit this one because its /
element=
{<div id='reactRouterTopRoute' className='eatZoomGesture'
style={{ fontSize:cssVariableFromCSSfile_fontsizeA}} >
{/* "Outlet" below says to include content in nested component */}
<Mobiwek61MenuTop mwmenuRoot={theMobiwekMenu} pageContent={<Outlet/>} />
</div>}
>
{/* within JSX, call "function" to setup routes. Note unusual syntax of enclosing inside {} */}
{ SetupRoutes_A() }
</Route>
</Routes>
</BrowserRouter>
);
/** sets up React routes for this application */
function SetupRoutes_A() {
return(<>
{/* // this wildcard route gets hit only when no other sibling routes get hit */}
<Route path='*'
element={<HandleByFullPathToFindMenuEntry mwmenuRoot={theMobiwekMenu} />}
/>
<Route path='about_app' element={ < Do_about_app /> } />
{/* this route send "itemKey" as a parameter to the Aircraft component.
itemKey could be "/x15/x15image" or "/x15/x15". Aircraft component decides what action to take */}
<Route path='x15/:itemKey'
element={ <Aircraft/>}
/>
<Route path='amhistory' element={<Outlet/>} >
<Route path='aircraft/:itemKey' element={ <Aircraft/> } />
</Route>
</>)
}
/** 1) Sets up mobiwekmenu whose branches and leaves are specified by props.mwmenuRoot
* 2) Sets up callback function which gets invoked when a leaf is clicked.
* Typically calls navigate() to point browser to new location indicated by what was clicked,
* but also check for special things like 'resetMenu'.
* 3) I'm called inside a <Route/> tag so I get to use useNavigate(). Else error occurs
*/
function Mobiwek61MenuTop(props) {
const navigate = useNavigate();
return(<>
<MobiWekMenuFrame
appTitle="MENU-DEMO"
mobiwekMenuJSON_A={ props.mwmenuRoot }
menuCallBackPtr={ ReactAppForDevelopmentMenuCallbackFn }
overflowCSS="hidden" //"scroll" // leads to div "divPageContentInFrameHelper" having this property
{...props} />
</>)
/**
* Callback function for menu LEAF choice
* @param mwmkeyFullPath unique mwmkey as defined in menuspec "theMobiwekMenu" includes hierarchy ie: workshopTools/power/saw/chainsaw
* @param innerText label of menu button
* @param fontSearchUrlParam ie: "?mwmfont=24.3px"
* to access from window: "new URLSearchParams(window.location.search).get('mwmfont')"
*/
function ReactAppForDevelopmentMenuCallbackFn(mwmkeyFullPath, innerText, fontSearchUrlParam) {
console.log('LEAF CLICKED-- mwmkey: ' + mwmkeyFullPath + ' label: ' + innerText.replaceAll('\n', '\\n') + ' fontSearchUrlParam ' + fontSearchUrlParam)
// console.log(' zzb ' + new URLSearchParams(window.location.search).get('mwmfont'))
if (mwmkeyFullPath === 'resetMenu') mwmkeyFullPath = '/'
if (mwmkeyFullPath === '/Home') mwmkeyFullPath = '/'
navigate(mwmkeyFullPath + fontSearchUrlParam);
}
}
/** "about" page */
function Do_about_app(props) {
useEffect(() => {
// The MobiWekPageWrapper component which wraps this component does CSS overflow "none" to accommodate
// zoomable scrollable images. This "page" is just html, so add scrolling as follows:
document.querySelector("#divPageContentInFrameHelper").style.overflow = "scroll";
}, []);
// JSX multi classname: <div className={devProjCSS.divInMiddle + ' ' + devProjCSS.textShadowB}
return(<div style={{ margin:'10vh 5px 5px 5px', padding:'5px',border:'3px solid #cccccc'}}>
<div style={{ marginBottom:'20px', color:'#ffff00'}}>This is a demo of mobiwek61menu. </div>
mobiwek61menu is an <b>npm package</b> which implements a floating overlay menu. <br/>
It is an experimental project to:
<ul>
<li>fit a large cascading menu onto a mobile screen</li>
<li>provide variable font size with one click</li>
<li>present user with a simple interface</li>
<li>present menu while minimally obscuring content, which may be artwork.</li>
<li>display zoomable images of historically significant handwriting to assist readability</li>
<li>present info button to display information and a qr code to that menu location</li>
</ul>
<div style={{ marginBottom:'20px', color:'#ffff00'}}>Related resources</div>
<ul>
<li>npm package location: <a style={{color:'#32ffbc'}} href='https://www.npmjs.com/package/mobiwek61menu'><>https://www.npmjs.com/package/mobiwek61menu</></a></li>
<li>provide variable font size with one click</li>
<li>another line</li><li>another line</li><li>another line</li><li>another line</li><li>another line</li>
</ul>
</div>)
}
/** depending of parameters, show specific webpage. If no match, decide if image is shown */
function Aircraft() {
var params = useParams();
// if (true) return(<>hello</>)
// obtain matching menu leaf (a JSON object) in theMobiwekMenu tree, as indicated by fullUrlPath
var fullUrlPath = useLocation().pathname;
var mwmkeyLeaf = findJsonObjectByFullPath(theMobiwekMenu, fullUrlPath)
if (params.itemKey === 'x15') {
console.log("Aircraft() handler, x15 page");
return MobiWekPageWrapper(Page_x15_spaceplane(mwmkeyLeaf))
//return page_x15_spaceplane(mwmkeyLeaf);
}
if (params.itemKey === 'spaceShuttleIntro') {
console.log("Aircraft() handler, spaceShuttleIntro page");
return MobiWekPageWrapper(Page_spaceShuttle(mwmkeyLeaf));
//return Page_spaceShuttle(mwmkeyLeaf)
}
// if leaf is an image, just give to MediaPictureWithInfo()
if (mwmkeyLeaf.mwmtype === 'image') {
console.log("Aircraft() handler, just am image");
return MediaPictureWithInfo(mwmkeyLeaf)
}
// get here only if there is a problem
return (<>ExperimentalAircraft no handler found</>)
}
/** custom page for x15. Includes clickable image to navigate to image display page. */
function Page_x15_spaceplane(mwmkeyLeaf) {
const navigate = useNavigate();
return <div><h1>x15 Spaceplane</h1><br/>first winged vehicle designed to go outside the atmosphere<br/>
<div style={{border: 'solid 0px red', display: 'inline-block'}}
onClick={ () => { const nav = getNavigateDir();
navigate(nav.locationDir + '/' + mwmkeyLeaf.clickMeLink + nav.fontSuffix)}} >
<img style={{border: 'solid 5px white', borderRadius:'48px', margin:'5vw', width:'70vw' }}
src='/jpeg/hist/aerospace/WIKIMEDIA-X-15_flying.jpg' alt='x15'
/></div></div>
}
/** pulls apart window.location to get current folder and get font-suffix thing.
* Otherwise navigate tries to go to subfolder of current url.
* Please let me know if there is a builtin way of doing this. */
function getNavigateDir() {
var locationDir = window.location.pathname;
locationDir = locationDir.substring(0, locationDir.lastIndexOf("/") );
const fontSuffix = window.location.search // ie: '?mwmfont=22.2px' .. need to stick back onto end
return { 'locationDir': locationDir, 'fontSuffix': fontSuffix }
}
/** custom page for space shuttle */
function Page_spaceShuttle(mwmkeyLeaf) {
const navigate = useNavigate();
// useEffect(() => {
// // document.querySelector('#clkImgA').addEventListener('click', () => { console.log('evtlisten')})
// }, []);
return <div>
<h1>Space Shuttle</h1>
First spacecraft designed with flying wings (maybe)<br/>
{/* image is not clickable so wrap in a div */}
<div style={{border: 'solid 0px red', display: 'inline-block'}}
onClick={ () => { const nav = getNavigateDir();
navigate(nav.locationDir + '/' + mwmkeyLeaf.clickMeLink + nav.fontSuffix)}} >
<img style={{border: 'solid 5px white', borderRadius:'48px', margin:'5vw', width:'70vw' }}
src='/jpeg/hist/aerospace/WIKIMEDIA-spaceShuttleTransport.jpg'
alt='space shuttle transported' />
</div><br/>Space shuttle transported on 747
<div onClick={ () => { const nav = getNavigateDir();
navigate(nav.locationDir + '/' + mwmkeyLeaf.clickMeLink2 + nav.fontSuffix)}} >
<img id='clkImgA' style={{border: 'solid 5px white', borderRadius:'48px', margin:'5vw', width:'70vw' }}
src='/jpeg/hist/aerospace/WIKIMEDIA-spaceShuttleOrbit.jpg' alt='space shuttle in orbit'/>
</div>
<br/>Space shuttle in orbit
</div>
}
/**
* Router handler uses router full path to find matching menu entry in menu object.
* it then examines mwmtype of menu entry to direct to proper handler.
* @param props.mwmenuRoot head of JSON menu structure to search for node having fullPath
*/
function HandleByFullPathToFindMenuEntry(props) {
// var fullUrlPath = useParams()["*"]; // works only for <Route path='*' element=...
var fullUrlPath = useLocation().pathname
// SAVE to test error message:
// fullUrlPath = fullUrlPath + 'zzz' // to test things
// fullUrlPath = 'LibCongress/pictszzzz/gwoodAmericanGothic'
var mwmkeyLeaf = findJsonObjectByFullPath(props.mwmenuRoot, fullUrlPath)
//console.log('found ' + mwmkeyLeaf.mwmkey + ' path ' + fullUrlPath)
if (mwmkeyLeaf.error !== undefined) {
console.log('WARNING ' + fullUrlPath + ' path not found in menu JSON object. mwmkeyLeaf.error: ' + mwmkeyLeaf.error)
return(<span className={devProjCSS.divInMiddle + ' ' + devProjCSS.textShadowB} ><br/>
No route set for path <br/>"{ fullUrlPath }" not found<br/>This url is probably for testing the menu display and is not
a bug. </span>)
}
if (mwmkeyLeaf.imgurl !== undefined)
return MediaPictureWithInfo(mwmkeyLeaf)
// if not just show a message
// following shows how JSX mixes variables with markuup and styles. A bit weird looking.
const redHi = { background:'#ff00001f', overflowWrap: 'anywhere' }
return(<div className={devProjCSS.divInMiddle + ' ' + devProjCSS.textShadowB}
style={{ ...redHi, margin:'0 7vw 0 7vw', width:"inherit", left:'2vw'}} > {/* WOW bing ai got this answer! */}
This menu leaf is probably test menu data with no handler setup, and is not a bug.
<br/><code>function HandleByFullPathToFindMenuEntry(). </code><br/> fullUrlPath: { fullUrlPath }
<br/>mwmkey: {mwmkeyLeaf.mwmkey}<br/>
</div>)
}
}
export { ReactAppForDevelopment };
-
INSPECT AI OUTPUT BEFORE PASTE into code because it is
- NOT HUMAN OUTPUT and may contain HORRENDOUS OUTPUT
- potentially RUNNABLE CODE AND SHOULD NOT BE TRUSTED. In my testing it has been innocuous JSON (so far).
- Menu composition worked for me using Copilot. I don't really know what's going on but the robot did what I needed to get a menu. LEAF links to to a placeholder handler in this demo; further coding is required in ReactAppForDevelopment.js
- I tried it on Copilot. Your query output (or whatever you call it on AI) needs to be in properly composed JSON matching this project, and you do this by specifying a JSON schema.
- To generate a JSON schema I fed AI some sample menu spec JSON and told it to generate. I needed to fiddle with it, but its a lot easier than coding a schema from scratch. See the successful schema below.
- I found I need to specify various things to push AI the right way to give me what I want, over many tries. AI occasionally decides to take an unwanted approach but this is fixed by you specifying further conditions.
- Below sample gave me usable results. Unlike sql queries, the result varies, maybe influenced by session history. It would be great if I could "lock" a query.
Add a comma after inserting the output.
This is what I pasted into Copilot to get a usable result:
make a hierarchical list of food categories. top levels are cocktails, meat, fish, fruit.
Depth will be 4 levels. output using this json schema:
{ "$schema": "http://json-schema.org/draft-04/schema#", "type": "object",
"properties": {
"BRANCH": { "type": "string" }, "LEAF": { "type": "string" }, "txtdesc": { "type": "string" },
"imgurl": { "type": "string" }, "mwmkey": { "type": "string" }, "mwmtype": { "type": "string" },
"items": { "type": "array", "items": { "$ref": "#/definitions/item" } }
},
"required": ["imgurl", "mwmkey", "mwmtype"],
"definitions": { "item":
{ "type": "object",
"properties": {
"BRANCH": { "type": "string" }, "LEAF": { "type": "string" }, "txtdesc": { "type": "string" },
"imgurl": { "type": "string" }, "mwmkey": { "type": "string" },
"mwmtype": { "type": "string" },
"items": { "type": "array", "items": { "$ref": "#/definitions/item" } }
},
"required": ["imgurl", "mwmkey", "mwmtype"], "dependencies": { "items": ["BRANCH"] }
}
}
}