Skip to content

Commit 77d0d67

Browse files
committed
Add show/copy source code feature to mapmlify-layer
Add toggle and copy buttons to the layer controls panel that let users view and copy the pretty-printed current state of the mapml-viewer markup. - Show/hide button toggles a readonly textarea below the map viewer - Copy button writes source code to clipboard via Clipboard API - Source code auto-updates on map-moveend events and control changes - Textarea auto-sizes to fit content without vertical scrollbars - Dynamic style elements injected by mapml-viewer are stripped from output - Pretty-printer replaces XMLSerializer for clean indented HTML output
1 parent 6bb7fae commit 77d0d67

2 files changed

Lines changed: 197 additions & 7 deletions

File tree

src/script/mapmlify-layer.js

Lines changed: 140 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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

src/style/main.css

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,3 +450,60 @@ mapml-viewer {
450450
border: 1px solid #ddd;
451451
border-radius: 4px;
452452
}
453+
454+
/* Source code controls */
455+
.source-code-controls {
456+
display: flex;
457+
gap: 0.5rem;
458+
margin-top: 0.75rem;
459+
flex-wrap: wrap;
460+
}
461+
462+
.source-toggle-btn,
463+
.source-copy-btn {
464+
padding: 0.4rem 0.75rem;
465+
font-size: 0.8rem;
466+
border-radius: 3px;
467+
cursor: pointer;
468+
border: 1px solid #ddd;
469+
flex: 1 1 auto;
470+
min-width: 0;
471+
white-space: nowrap;
472+
}
473+
474+
.source-toggle-btn {
475+
background-color: #ecf0f1;
476+
color: #2c3e50;
477+
}
478+
479+
.source-toggle-btn:hover {
480+
background-color: #d5dbdb;
481+
}
482+
483+
.source-copy-btn {
484+
background-color: #3498db;
485+
color: white;
486+
border-color: #3498db;
487+
}
488+
489+
.source-copy-btn:hover {
490+
background-color: #2980b9;
491+
}
492+
493+
.source-code-textarea {
494+
width: 100%;
495+
margin-top: 0.5rem;
496+
padding: 0.75rem;
497+
font-family: 'Courier New', Courier, monospace;
498+
font-size: 0.8rem;
499+
line-height: 1.4;
500+
border: 1px solid #ddd;
501+
border-radius: 4px;
502+
background-color: #f8f9fa;
503+
color: #2c3e50;
504+
resize: none;
505+
white-space: pre;
506+
overflow-y: hidden;
507+
overflow-x: auto;
508+
tab-size: 2;
509+
}

0 commit comments

Comments
 (0)