@@ -3,21 +3,24 @@ import { prepareExample } from './examples.js';
33
44const assetGeneration = new URL ( import . meta. url ) . search ;
55const { buildProject } = await import ( `./project-export.js${ assetGeneration } ` ) ;
6+ const { readSelection, validateSource } = await import ( `./local-files.js${ assetGeneration } ` ) ;
67
78const el = id => document . getElementById ( `pg-${ id } ` ) ;
89const html = el ( 'html' ) , css = el ( 'css' ) , picker = el ( 'example' ) ;
910const projectLanguage = el ( 'project-language' ) ;
1011const originals = new Map ( ) , edits = new Map ( ) ;
1112let current = 'invoice' , worker = null , deadline = null , pageIndex = 0 , pages = [ ] , pdfUrl = null ;
1213let renderedSource = null ;
14+ let localAssets = [ ] , assetVersion = 0 ;
1315
1416function status ( message , error = false ) {
1517 el ( 'status' ) . textContent = message ;
1618 el ( 'status' ) . dataset . error = String ( error ) ;
1719}
1820
1921function changedSinceRender ( ) {
20- return ! renderedSource || renderedSource . html !== html . value || renderedSource . css !== css . value ;
22+ return ! renderedSource || renderedSource . html !== html . value || renderedSource . css !== css . value
23+ || renderedSource . assetVersion !== assetVersion ;
2124}
2225
2326function setDownload ( enabled ) {
@@ -71,7 +74,7 @@ function fail(message) {
7174
7275function startRender ( ) {
7376 if ( worker ) finish ( ) ;
74- const source = { html : html . value , css : css . value } ;
77+ const source = { html : html . value , css : css . value , assets : localAssets , assetVersion } ;
7578 if ( new TextEncoder ( ) . encode ( source . html + source . css ) . length > 200000 ) {
7679 status ( 'This playground accepts up to 200 KB of HTML and CSS combined.' , true ) ;
7780 return ;
@@ -113,7 +116,7 @@ function startRender() {
113116 finish ( ) ;
114117 setDownload ( ! changedSinceRender ( ) ) ;
115118 if ( changedSinceRender ( ) ) status ( 'Source changed during rendering. Render again to update the PDF.' ) ;
116- else if ( data . report . missing_glyphs ) status ( `PDF created, but ${ data . report . missing_glyphs } character(s) have no glyph in the bundled fonts. Review the output before using it.` , true ) ;
119+ else if ( data . report . missing_glyphs ) status ( `PDF created, but ${ data . report . missing_glyphs } character(s) have no glyph in the available fonts. Review the output before using it.` , true ) ;
117120 else status ( 'Your PDF is ready. Change the design, or download it and keep building.' ) ;
118121 } ;
119122 worker . postMessage ( source ) ;
@@ -138,6 +141,70 @@ function saveText(name, content, type) {
138141 setTimeout ( ( ) => URL . revokeObjectURL ( url ) , 1000 ) ;
139142}
140143
144+ function showAssets ( ) {
145+ const list = el ( 'assets' ) ;
146+ list . replaceChildren ( ) ;
147+ el ( 'asset-count' ) . textContent = `${ localAssets . length } local ${ localAssets . length === 1 ? 'asset' : 'assets' } ` ;
148+ el ( 'asset-help' ) . hidden = localAssets . length === 0 ;
149+ for ( const asset of localAssets ) {
150+ const row = document . createElement ( 'li' ) , name = document . createElement ( 'code' ) , remove = document . createElement ( 'button' ) ;
151+ name . textContent = `assets/${ asset . name } ` ;
152+ remove . textContent = 'Remove' ;
153+ remove . setAttribute ( 'aria-label' , `Remove ${ asset . name } ` ) ;
154+ remove . addEventListener ( 'click' , ( ) => {
155+ localAssets = localAssets . filter ( item => item !== asset ) ;
156+ assetVersion ++ ;
157+ showAssets ( ) ;
158+ setDownload ( ! changedSinceRender ( ) ) ;
159+ status ( 'Asset removed. Render again to update the PDF.' ) ;
160+ el ( 'open-files' ) . focus ( ) ;
161+ } ) ;
162+ row . append ( name , remove ) ;
163+ list . append ( row ) ;
164+ }
165+ }
166+
167+ el ( 'open-files' ) . addEventListener ( 'click' , ( ) => el ( 'files' ) . click ( ) ) ;
168+ el ( 'files' ) . addEventListener ( 'change' , async ( ) => {
169+ const files = Array . from ( el ( 'files' ) . files ) ;
170+ if ( ! files . length ) return ;
171+ const snapshot = { current, html : html . value , css : css . value , assetVersion } ;
172+ el ( 'open-files' ) . disabled = true ;
173+ status ( 'Opening selected files on your device…' ) ;
174+ try {
175+ const selected = await readSelection ( files , localAssets ) ;
176+ if ( current !== snapshot . current || html . value !== snapshot . html || css . value !== snapshot . css || assetVersion !== snapshot . assetVersion ) {
177+ throw new Error ( 'Your document changed while the files were opening. Choose the files again to apply them.' ) ;
178+ }
179+ const hasSource = selected . html !== null || selected . css !== null ;
180+ const nextHtml = selected . html ?? html . value ;
181+ const nextCss = selected . css ?? ( selected . html !== null ? '' : css . value ) ;
182+ if ( hasSource ) validateSource ( nextHtml , nextCss ) ;
183+ if ( worker ) finish ( ) ;
184+ if ( hasSource ) {
185+ edits . set ( current , { html : html . value , css : css . value } ) ;
186+ current = 'custom' ;
187+ originals . set ( current , { html : nextHtml , css : nextCss } ) ;
188+ edits . delete ( current ) ;
189+ if ( ! picker . querySelector ( 'option[value="custom"]' ) ) picker . add ( new Option ( 'Your document' , 'custom' ) ) ;
190+ picker . value = current ;
191+ html . value = nextHtml ; css . value = nextCss ;
192+ el ( 'reset' ) . textContent = 'Reset document' ;
193+ selectTab ( selected . html !== null ? 'html' : 'css' ) ;
194+ }
195+ localAssets = [ ...localAssets , ...selected . assets ] ;
196+ assetVersion ++ ;
197+ showAssets ( ) ;
198+ clearOutput ( ) ;
199+ status ( 'Files opened locally. Use assets/filename for images and font files, then Render PDF.' ) ;
200+ } catch ( error ) {
201+ status ( error . message || 'Could not open the selected files.' , true ) ;
202+ } finally {
203+ el ( 'files' ) . value = '' ;
204+ el ( 'open-files' ) . disabled = false ;
205+ }
206+ } ) ;
207+
141208for ( const language of [ 'html' , 'css' ] ) {
142209 el ( `${ language } -tab` ) . addEventListener ( 'click' , ( ) => selectTab ( language ) ) ;
143210 el ( `${ language } -tab` ) . addEventListener ( 'keydown' , event => {
@@ -168,7 +235,7 @@ el('next').addEventListener('click', () => showPage(pageIndex + 1));
168235el ( 'project' ) . addEventListener ( 'click' , async ( ) => {
169236 const button = el ( 'project' ) , message = el ( 'project-status' ) ;
170237 // Capture edits before downloading assets, even if the user keeps typing or switches examples.
171- const snapshot = { name : current , html : html . value , css : css . value , language : projectLanguage ?. value ?? 'python' } ;
238+ const snapshot = { name : current , html : html . value , css : css . value , language : projectLanguage ?. value ?? 'python' , assets : localAssets , assetVersion } ;
172239 button . disabled = true ;
173240 button . textContent = 'Preparing project…' ;
174241 message . hidden = false ;
@@ -177,7 +244,8 @@ el('project').addEventListener('click', async () => {
177244 try {
178245 const bytes = await buildProject ( snapshot ) ;
179246 saveText ( `fullbleed-${ snapshot . name } -${ snapshot . language === 'node' ? 'node-' : '' } project.zip` , bytes , 'application/zip' ) ;
180- const newerEdits = current !== snapshot . name || html . value !== snapshot . html || css . value !== snapshot . css || ( projectLanguage ?. value ?? 'python' ) !== snapshot . language ;
247+ const newerEdits = current !== snapshot . name || html . value !== snapshot . html || css . value !== snapshot . css
248+ || assetVersion !== snapshot . assetVersion || ( projectLanguage ?. value ?? 'python' ) !== snapshot . language ;
181249 message . textContent = newerEdits
182250 ? 'Project downloaded with the source and language from when you clicked. Download again to include newer changes.'
183251 : 'Project downloaded. Extract the ZIP and follow its README to render locally.' ;
@@ -199,6 +267,7 @@ picker.addEventListener('change', () => {
199267 if ( worker ) finish ( ) ;
200268 edits . set ( current , { html : html . value , css : css . value } ) ;
201269 current = picker . value ;
270+ el ( 'reset' ) . textContent = current === 'custom' ? 'Reset document' : 'Reset example' ;
202271 const source = edits . get ( current ) || originals . get ( current ) ;
203272 html . value = source . html ; css . value = source . css ;
204273 clearOutput ( ) ; startRender ( ) ;
@@ -218,7 +287,7 @@ try {
218287 if ( originals . has ( hashExample ) ) current = hashExample ;
219288 picker . value = current ;
220289 html . value = originals . get ( current ) . html ; css . value = originals . get ( current ) . css ;
221- for ( const control of [ html , css , picker , el ( 'reset' ) , el ( 'render' ) , el ( 'project' ) , projectLanguage ] . filter ( Boolean ) ) control . disabled = false ;
290+ for ( const control of [ html , css , picker , el ( 'reset' ) , el ( 'render' ) , el ( 'project' ) , el ( 'open-files' ) , projectLanguage ] . filter ( Boolean ) ) control . disabled = false ;
222291 for ( const language of [ 'html' , 'css' ] ) { el ( `save-${ language } ` ) . href = '#' ; el ( `save-${ language } ` ) . setAttribute ( 'aria-disabled' , 'false' ) ; }
223292 startRender ( ) ;
224293} catch ( error ) { status ( error . message , true ) ; document . querySelector ( '.pg-output' ) . setAttribute ( 'aria-busy' , 'false' ) ; }
0 commit comments