Skip to content

Commit c8721a5

Browse files
committed
Add Web view tab for HTML/XHTML snippets and transform outputs
- Add synthetic "Web view" language tab when an example snippet has language html/xml and a url field (analogous to "Map view" for GeoJSON) - Add "Web" toggle button in transform output panel when output media type is text/html or application/xhtml+xml - Render content via SandboxedIframe (sandbox="allow-same-origin") which auto-sizes to content height on load, capped at 600px, with a resize handle for manual adjustment; falls back to 300px for cross-origin URLs
1 parent 624e12d commit c8721a5

4 files changed

Lines changed: 79 additions & 8 deletions

File tree

src/components/bblock/BuildingBlockExamples.vue

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@
4949

5050
<script setup>
5151
import {defineAsyncComponent, onMounted, ref, watch} from 'vue';
52-
import {knownLanguages, geoJsonLanguageIds} from "@/models/mime-types";
52+
import {knownLanguages, geoJsonLanguageIds, htmlLanguageIds} from "@/models/mime-types";
5353
import {useNavigationStore} from "@/stores/navigation";
54+
import LanguageTabs from "@/components/bblock/LanguageTabs.vue";
5455
5556
const ExampleViewer = defineAsyncComponent(() => import("@/components/bblock/ExampleViewer.vue"));
56-
const LanguageTabs = defineAsyncComponent(() => import("@/components/bblock/LanguageTabs.vue"));
5757
5858
const props = defineProps({
5959
bblock: Object,
@@ -122,6 +122,13 @@ function processExamples() {
122122
exampleLanguageTabs.push({id: 'map-view', order: -1, label: 'Map view'});
123123
}
124124
125+
const htmlSnippet = example.snippets?.find(snippet =>
126+
htmlLanguageIds.has(snippet.language?.id) && /^https?:\/\//.test(snippet.url)
127+
);
128+
if (htmlSnippet) {
129+
exampleLanguageTabs.push({id: 'web-view', order: -1, label: 'Web view'});
130+
}
131+
125132
if (props.bblock.transforms?.length) {
126133
const transformEntries = [];
127134
props.bblock.transforms.forEach(transform => {
@@ -171,7 +178,7 @@ function processExamples() {
171178
a.order === b.order ? a.label.localeCompare(b.label) : a.order - b.order
172179
);
173180
newExpandedExamples.push(exampleIdx);
174-
newSelectedLanguageTabs[exampleIdx] = exampleLanguageTabs.find(e => e.id !== 'map-view')?.id;
181+
newSelectedLanguageTabs[exampleIdx] = exampleLanguageTabs.find(e => e.id !== 'map-view' && e.id !== 'web-view')?.id;
175182
newLanguageTabs[exampleIdx] = exampleLanguageTabs;
176183
});
177184

src/components/bblock/ExampleViewer.vue

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@
122122
:ld-context="bblock.ldContext"
123123
/>
124124
</div>
125+
<sandboxed-iframe
126+
v-else-if="transformOutputView === 'web' && transformOutputIsHtml && language.transformEntry.url"
127+
:src="language.transformEntry.url"
128+
/>
125129
<div v-else style="max-height: 30em; overflow-y: auto">
126130
<code-viewer
127131
:code="transformOutputStatus.contents"
@@ -136,14 +140,15 @@
136140
</div>
137141
<div class="d-flex align-center mt-1" v-if="language.transformEntry.success && transformOutputStatus.contents">
138142
<v-btn-toggle
139-
v-if="transformOutputGeoJson"
143+
v-if="transformOutputGeoJson || transformOutputIsHtml"
140144
v-model="transformOutputView"
141145
mandatory
142146
density="compact"
143147
rounded="1"
144148
>
145149
<v-btn value="code" size="small" prepend-icon="mdi-code-tags">Code</v-btn>
146-
<v-btn value="map" size="small" prepend-icon="mdi-map">Map</v-btn>
150+
<v-btn v-if="transformOutputGeoJson" value="map" size="small" prepend-icon="mdi-map">Map</v-btn>
151+
<v-btn v-if="transformOutputIsHtml" value="web" size="small" prepend-icon="mdi-web">Web</v-btn>
147152
</v-btn-toggle>
148153
<v-spacer />
149154
<copy-to-clipboard-button :text="transformOutputStatus.contents" color="primary" variant="flat">Copy to clipboard</copy-to-clipboard-button>
@@ -180,6 +185,9 @@
180185
<geo-json-map-viewer :geojson="geoJsonData" :ld-context="bblock.ldContext"></geo-json-map-viewer>
181186
</div>
182187
</template>
188+
<template v-else-if="isWebView && webViewUrl">
189+
<sandboxed-iframe :src="webViewUrl" />
190+
</template>
183191
<template v-else-if="currentSnippet">
184192
<div style="max-height: 30em; overflow-y: auto">
185193
<code-viewer
@@ -268,12 +276,13 @@ import JsonLdIcon from '@/assets/json-ld-data-white.svg';
268276
import MarkdownText from "@/components/MarkdownText.vue";
269277
import GeoJsonMapViewer from "@/components/bblock/GeoJsonMapViewer.vue";
270278
import TransformInfo from "@/components/bblock/TransformInfo.vue";
271-
import { geoJsonLanguageIds } from "@/models/mime-types";
279+
import { geoJsonLanguageIds, htmlLanguageIds } from "@/models/mime-types";
272280
import { getTypeColor } from "@/models/transforms";
273281
import { useFetchDocumentByUrl } from "@/composables/bblock";
274282
import { useBBlockNavigation } from "@/composables/bblock-navigation";
275283
import CopyToClipboardButton from "@/components/CopyToClipboardButton.vue";
276284
import ProfilesValidationReportDialog from "@/components/bblock/ProfilesValidationReportDialog.vue";
285+
import SandboxedIframe from "@/components/bblock/SandboxedIframe.vue";
277286
import bblockService from "@/services/bblock.service";
278287
279288
const props = defineProps({
@@ -309,10 +318,18 @@ watch(() => props.language?.transformEntry?.profilesValidation, async (pv) => {
309318
}, { immediate: true });
310319
311320
const isMapView = computed(() => props.language?.id === 'map-view');
321+
const isWebView = computed(() => props.language?.id === 'web-view');
312322
const isTransformView = computed(() => props.language?.isTransform === true);
313323
324+
const webViewUrl = computed(() => {
325+
if (!isWebView.value) return null;
326+
return props.example?.snippets?.find(s =>
327+
htmlLanguageIds.has(s.language?.id) && /^https?:\/\//.test(s.url)
328+
)?.url ?? null;
329+
});
330+
314331
const currentSnippet = computed(() => {
315-
if (isTransformView.value) return null;
332+
if (isTransformView.value || isWebView.value) return null;
316333
return (props.language && props.example?.snippets?.find(s => !s.language || s.language.id === props.language.id))
317334
|| props.example?.snippets?.[0];
318335
});
@@ -362,6 +379,17 @@ const transformOutputStatus = reactive(useFetchDocumentByUrl(
362379
computed(() => props.language?.transformEntry?.url ?? null)
363380
));
364381
382+
const htmlMimeTypes = new Set(['text/html', 'application/xhtml+xml']);
383+
384+
const transformOutputIsHtml = computed(() => {
385+
const mediaTypes = props.language?.transform?.outputs?.mediaTypes;
386+
if (!mediaTypes?.length) return false;
387+
return mediaTypes.some(mt => {
388+
const mime = typeof mt === 'string' ? mt : mt?.mimeType;
389+
return mime && htmlMimeTypes.has(mime);
390+
});
391+
});
392+
365393
const transformOutputGeoJson = computed(() => {
366394
if (!transformOutputStatus.contents) return null;
367395
try {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<template>
2+
<iframe
3+
ref="iframeEl"
4+
:src="src"
5+
sandbox="allow-same-origin"
6+
:style="{ width: '100%', height: height + 'px', border: 'none', display: 'block', resize: 'vertical', overflow: 'auto' }"
7+
@load="onLoad"
8+
></iframe>
9+
</template>
10+
<script setup>
11+
import { ref } from 'vue';
12+
13+
const props = defineProps({
14+
src: { type: String, required: true },
15+
fallbackHeight: { type: Number, default: 300 },
16+
maxHeight: { type: Number, default: 600 },
17+
});
18+
19+
const iframeEl = ref(null);
20+
const height = ref(props.fallbackHeight);
21+
22+
function onLoad() {
23+
try {
24+
const doc = iframeEl.value?.contentDocument;
25+
if (!doc) return;
26+
const h = doc.documentElement?.scrollHeight || doc.body?.scrollHeight;
27+
if (h) {
28+
height.value = Math.min(h, props.maxHeight);
29+
}
30+
} catch {
31+
// cross-origin — keep fallback height
32+
}
33+
}
34+
</script>

src/models/mime-types.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,6 @@ const getHighlightLanguage = lang => {
109109

110110
const geoJsonLanguageIds = new Set(['json', 'jsonld', 'geojson']);
111111

112-
export { knownLanguages, getHighlightLanguage, geoJsonLanguageIds };
112+
const htmlLanguageIds = new Set(['html', 'xml']);
113+
114+
export { knownLanguages, getHighlightLanguage, geoJsonLanguageIds, htmlLanguageIds };

0 commit comments

Comments
 (0)