@@ -28,6 +28,9 @@ class MapmlifyLayer extends HTMLElement {
2828 #layerCheckbox = null ;
2929 #viewerContainer = null ;
3030 #preview = null ;
31+ #sourceCodeTextarea = null ;
32+ #sourceCodeVisible = false ;
33+ #moveendHandler = null ;
3134
3235 set layerConfig ( value ) {
3336 this . #config = value ;
@@ -108,6 +111,7 @@ class MapmlifyLayer extends HTMLElement {
108111 this . #boundsEnabled = true ;
109112 this . #queryEnabled = false ;
110113 this . #viewerActive = false ;
114+ this . #sourceCodeVisible = false ;
111115 this . #selectedExportMode = 'individual' ;
112116 }
113117
@@ -407,6 +411,49 @@ class MapmlifyLayer extends HTMLElement {
407411 }
408412 } ) ;
409413
414+ // Source code buttons
415+ const sourceCodeDiv = document . createElement ( 'div' ) ;
416+ sourceCodeDiv . className = 'source-code-controls' ;
417+
418+ const toggleBtn = document . createElement ( 'button' ) ;
419+ toggleBtn . className = 'source-toggle-btn' ;
420+ toggleBtn . textContent = 'Show source code' ;
421+ toggleBtn . addEventListener ( 'click' , ( ) => {
422+ this . #sourceCodeVisible = ! this . #sourceCodeVisible;
423+ toggleBtn . textContent = this . #sourceCodeVisible
424+ ? 'Hide source code'
425+ : 'Show source code' ;
426+ if ( this . #sourceCodeTextarea) {
427+ this . #sourceCodeTextarea. style . display = this . #sourceCodeVisible
428+ ? 'block'
429+ : 'none' ;
430+ }
431+ if ( this . #sourceCodeVisible) this . #updateSourceCode( ) ;
432+ } ) ;
433+
434+ const copyBtn = document . createElement ( 'button' ) ;
435+ copyBtn . className = 'source-copy-btn' ;
436+ copyBtn . textContent = 'Copy source code' ;
437+ copyBtn . addEventListener ( 'click' , ( ) => {
438+ if ( ! this . #sourceCodeTextarea) return ;
439+ const text = this . #sourceCodeTextarea. value ;
440+ navigator . clipboard . writeText ( text ) . then (
441+ ( ) => {
442+ copyBtn . textContent = 'Copied!' ;
443+ setTimeout ( ( ) => {
444+ copyBtn . textContent = 'Copy source code' ;
445+ } , 1500 ) ;
446+ } ,
447+ ( ) => {
448+ // Fallback: select the text so user can Ctrl+C
449+ this . #sourceCodeTextarea. select ( ) ;
450+ }
451+ ) ;
452+ } ) ;
453+
454+ sourceCodeDiv . append ( toggleBtn , copyBtn ) ;
455+ controls . appendChild ( sourceCodeDiv ) ;
456+
410457 this . appendChild ( controls ) ;
411458
412459 // ── Viewer / preview panel ──
@@ -424,6 +471,15 @@ class MapmlifyLayer extends HTMLElement {
424471 this . #preview = preview ;
425472 container . appendChild ( preview ) ;
426473
474+ // Source code textarea (hidden by default)
475+ const sourceTextarea = document . createElement ( 'textarea' ) ;
476+ sourceTextarea . className = 'source-code-textarea' ;
477+ sourceTextarea . readOnly = true ;
478+ sourceTextarea . style . display = 'none' ;
479+ sourceTextarea . setAttribute ( 'aria-label' , 'Map source code' ) ;
480+ this . #sourceCodeTextarea = sourceTextarea ;
481+ container . appendChild ( sourceTextarea ) ;
482+
427483 this . appendChild ( container ) ;
428484
429485 // ── Wire layer checkbox ──
@@ -449,6 +505,7 @@ class MapmlifyLayer extends HTMLElement {
449505 #onQueryChange( ) {
450506 if ( ! this . #viewerActive) return ;
451507 this . #updateQueryInViewer( ) ;
508+ this . #updateSourceCode( ) ;
452509 }
453510
454511 // ─── PREVIEW ──────────────────────────────────────────
@@ -586,7 +643,18 @@ class MapmlifyLayer extends HTMLElement {
586643 // Add data layer
587644 this . #addDataLayerToViewer( viewer ) ;
588645
589- container . appendChild ( viewer ) ;
646+ // Insert viewer before the textarea so it appears above it
647+ if ( this . #sourceCodeTextarea && this . #sourceCodeTextarea. parentNode === container ) {
648+ container . insertBefore ( viewer , this . #sourceCodeTextarea) ;
649+ } else {
650+ container . appendChild ( viewer ) ;
651+ }
652+
653+ // Listen for moveend to update source code
654+ this . #moveendHandler = ( ) => this . #updateSourceCode( ) ;
655+ viewer . addEventListener ( 'map-moveend' , this . #moveendHandler) ;
656+ // Initial serialization
657+ this . #updateSourceCode( ) ;
590658 }
591659
592660 #removeViewer( ) {
@@ -596,7 +664,18 @@ class MapmlifyLayer extends HTMLElement {
596664 if ( this . #preview) this . #preview. style . display = 'block' ;
597665
598666 const viewer = container . querySelector ( 'mapml-viewer' ) ;
599- if ( viewer ) viewer . remove ( ) ;
667+ if ( viewer ) {
668+ if ( this . #moveendHandler) {
669+ viewer . removeEventListener ( 'map-moveend' , this . #moveendHandler) ;
670+ this . #moveendHandler = null ;
671+ }
672+ viewer . remove ( ) ;
673+ }
674+
675+ // Clear source code
676+ if ( this . #sourceCodeTextarea) {
677+ this . #sourceCodeTextarea. value = '' ;
678+ }
600679 }
601680
602681 // ─── BASEMAP ────────────────────────────────────────────
@@ -1778,16 +1857,70 @@ class MapmlifyLayer extends HTMLElement {
17781857 return d . innerHTML ;
17791858 }
17801859
1860+ // ─── SOURCE CODE ──────────────────────────────────────
1861+
1862+ #updateSourceCode( ) {
1863+ if ( ! this . #sourceCodeVisible) return ;
1864+ const viewer = this . #viewerContainer?. querySelector ( 'mapml-viewer' ) ;
1865+ if ( ! viewer || ! this . #sourceCodeTextarea) return ;
1866+ this . #sourceCodeTextarea. value = this . #serializeViewer( viewer ) ;
1867+ // Auto-size to fit content without scrollbars
1868+ this . #sourceCodeTextarea. style . height = 'auto' ;
1869+ this . #sourceCodeTextarea. style . height = this . #sourceCodeTextarea. scrollHeight + 'px' ;
1870+ }
1871+
1872+ #serializeViewer( viewer ) {
1873+ const clone = viewer . cloneNode ( true ) ;
1874+ // Sync live attributes (zoom, lat, lon) from the viewer
1875+ clone . setAttribute ( 'zoom' , viewer . getAttribute ( 'zoom' ) ) ;
1876+ clone . setAttribute ( 'lat' , viewer . getAttribute ( 'lat' ) ) ;
1877+ clone . setAttribute ( 'lon' , viewer . getAttribute ( 'lon' ) ) ;
1878+ // Remove dynamic style elements injected by mapml-viewer
1879+ clone . querySelectorAll ( 'style' ) . forEach ( ( s ) => s . remove ( ) ) ;
1880+ return this . #prettyPrint( clone ) ;
1881+ }
1882+
1883+ #prettyPrint( node , indent = '' ) {
1884+ const INDENT = ' ' ;
1885+ if ( node . nodeType === Node . TEXT_NODE ) {
1886+ const text = node . textContent . trim ( ) ;
1887+ return text ? indent + text + '\n' : '' ;
1888+ }
1889+ if ( node . nodeType !== Node . ELEMENT_NODE ) return '' ;
1890+
1891+ const tag = node . tagName . toLowerCase ( ) ;
1892+ let attrs = '' ;
1893+ for ( const attr of node . attributes ) {
1894+ attrs += ` ${ attr . name } ="${ attr . value } "` ;
1895+ }
1896+
1897+ if ( ! node . childNodes . length ) {
1898+ return `${ indent } <${ tag } ${ attrs } ></${ tag } >\n` ;
1899+ }
1900+
1901+ // Single text child
1902+ if (
1903+ node . childNodes . length === 1 &&
1904+ node . firstChild . nodeType === Node . TEXT_NODE
1905+ ) {
1906+ const text = node . firstChild . textContent . trim ( ) ;
1907+ return `${ indent } <${ tag } ${ attrs } >${ text } </${ tag } >\n` ;
1908+ }
1909+
1910+ let result = `${ indent } <${ tag } ${ attrs } >\n` ;
1911+ for ( const child of node . childNodes ) {
1912+ result += this . #prettyPrint( child , indent + INDENT ) ;
1913+ }
1914+ result += `${ indent } </${ tag } >\n` ;
1915+ return result ;
1916+ }
1917+
17811918 // ─── PUBLIC API ────────────────────────────────────────
17821919
17831920 getMapMLMarkup ( ) {
17841921 const viewer = this . #viewerContainer?. querySelector ( 'mapml-viewer' ) ;
17851922 if ( ! viewer ) return null ;
1786- // Clone and serialize
1787- const clone = viewer . cloneNode ( true ) ;
1788- // Pretty-print with indentation
1789- const serializer = new XMLSerializer ( ) ;
1790- return serializer . serializeToString ( clone ) ;
1923+ return this . #serializeViewer( viewer ) ;
17911924 }
17921925}
17931926
0 commit comments