From b6f0ff78a3e64fb0d36533633b81ee44e71b5da6 Mon Sep 17 00:00:00 2001 From: jamilahmadzai Date: Mon, 25 May 2026 19:55:41 +0200 Subject: [PATCH] Add configurable pane widths --- docs/embed.md | 20 ++++++ package.json | 1 + src/components/ConsolePan.vue | 7 ++- src/components/OutputPan.vue | 6 +- src/store/index.js | 13 +++- src/utils/create-pan.js | 7 ++- src/utils/pan-position.js | 10 ++- src/utils/pan-widths.js | 115 ++++++++++++++++++++++++++++++++++ src/views/EditorPage.vue | 8 ++- test/pan-widths.js | 39 ++++++++++++ 10 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 src/utils/pan-widths.js create mode 100644 test/pan-widths.js diff --git a/docs/embed.md b/docs/embed.md index 9b190eda..10ae6711 100644 --- a/docs/embed.md +++ b/docs/embed.md @@ -1,3 +1,23 @@ You can embed the URL using an iframe in your website. Optionally append `?readonly` to make the editor read-only. + +Use `show` with `widths` to control which panes are visible and how much +horizontal space each pane gets: + +```html + +``` + +The `widths` value is a comma-separated list that matches the visible pane +order: HTML, CSS, JS, Console, Output. Values are relative, so `1,1,2` and +`25,25,50` produce the same layout. + +For order-independent embeds, use named pane widths: + +```html + +``` + +If `widths` is missing, invalid, or does not cover every visible pane, CodePan +keeps the default equal-width layout. diff --git a/package.json b/package.json index 7ba66a1b..f1181a8e 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "repository": {}, "scripts": { "test": "npm run lint", + "test:pan-widths": "node test/pan-widths.js", "lint": "xo", "dev": "poi", "build": "poi build", diff --git a/src/components/ConsolePan.vue b/src/components/ConsolePan.vue index d9df33b3..3161be1b 100644 --- a/src/components/ConsolePan.vue +++ b/src/components/ConsolePan.vue @@ -56,8 +56,11 @@ visiblePans: { immediate: true, handler(val) { - this.style = panPosition(val, 'console') + this.style = panPosition(val, 'console', this.panWidths) } + }, + panWidths(val) { + this.style = panPosition(this.visiblePans, 'console', val) } }, mounted() { @@ -69,7 +72,7 @@ }) }, computed: { - ...mapState(['logs', 'visiblePans', 'activePan']), + ...mapState(['logs', 'visiblePans', 'activePan', 'panWidths']), enableResizer() { return hasNextPan(this.visiblePans, 'console') }, diff --git a/src/components/OutputPan.vue b/src/components/OutputPan.vue index 16084e3b..038dbbcf 100644 --- a/src/components/OutputPan.vue +++ b/src/components/OutputPan.vue @@ -76,8 +76,11 @@ export default { visiblePans: { immediate: true, handler(val) { - this.style = panPosition(val, 'output') + this.style = panPosition(val, 'output', this.panWidths) } + }, + panWidths(val) { + this.style = panPosition(this.visiblePans, 'output', val) } }, computed: { @@ -86,6 +89,7 @@ export default { 'css', 'html', 'visiblePans', + 'panWidths', 'activePan', 'githubToken', 'iframeStatus' diff --git a/src/store/index.js b/src/store/index.js index dd52913f..359dcf26 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -17,6 +17,7 @@ import progress from 'nprogress' import api from '@/utils/github-api' import req from 'reqjs' import Event from '@/utils/event' +import { parsePanWidths } from '@/utils/pan-widths' Vue.use(Vuex) @@ -75,7 +76,9 @@ const store = new Vuex.Store({ userMeta: JSON.parse(localStorage.getItem('codepan:user-meta')) || {}, editorStatus: 'saved', iframeStatus: null, - transforming: false + transforming: false, + panWidthsQuery: null, + panWidths: null }, mutations: { UPDATE_CODE(state, { type, code }) { @@ -102,6 +105,11 @@ const store = new Vuex.Store({ }, SHOW_PANS(state, pans) { state.visiblePans = sortPans(pans) + state.panWidths = parsePanWidths(state.panWidthsQuery, state.visiblePans) + }, + SET_PAN_WIDTHS(state, value) { + state.panWidthsQuery = value + state.panWidths = parsePanWidths(value, state.visiblePans) }, ACTIVE_PAN(state, pan) { state.activePan = pan @@ -150,6 +158,9 @@ const store = new Vuex.Store({ showPans({ commit }, pans) { commit('SHOW_PANS', pans) }, + setPanWidths({ commit }, value) { + commit('SET_PAN_WIDTHS', value) + }, async updateTransformer({ commit }, { type, transformer }) { if ( transformer === 'babel' || diff --git a/src/utils/create-pan.js b/src/utils/create-pan.js index fc74fdad..19f5b9d8 100644 --- a/src/utils/create-pan.js +++ b/src/utils/create-pan.js @@ -17,7 +17,7 @@ export default ({ name, editor, components } = {}) => { } }, computed: { - ...mapState([name, 'visiblePans', 'activePan', 'autoRun']), + ...mapState([name, 'visiblePans', 'activePan', 'autoRun', 'panWidths']), ...mapState({ isVisible: state => state.visiblePans.indexOf(name) !== -1 }), @@ -38,9 +38,12 @@ export default ({ name, editor, components } = {}) => { visiblePans: { immediate: true, handler(val) { - this.style = panPosition(val, name) + this.style = panPosition(val, name, this.panWidths) } }, + panWidths(val) { + this.style = panPosition(this.visiblePans, name, val) + }, [`${name}.transformer`](val) { const mode = getEditorModeByTransfomer(val) this.editor.setOption('mode', mode) diff --git a/src/utils/pan-position.js b/src/utils/pan-position.js index 50ab4257..db1399de 100644 --- a/src/utils/pan-position.js +++ b/src/utils/pan-position.js @@ -1,4 +1,12 @@ -export default (pans, pan) => { +import { getPanPosition } from '@/utils/pan-widths' + +export default (pans, pan, widths) => { + const customPosition = getPanPosition(pans, pan, widths) + + if (customPosition) { + return customPosition + } + const panWidth = 100 / pans.length const pansCount = matchedPans => { return pans.filter(p => { diff --git a/src/utils/pan-widths.js b/src/utils/pan-widths.js new file mode 100644 index 00000000..1bda55a5 --- /dev/null +++ b/src/utils/pan-widths.js @@ -0,0 +1,115 @@ +const panNames = ['html', 'css', 'js', 'console', 'output'] + +const normalizeQueryValue = value => { + if (Array.isArray(value)) { + return value[0] + } + + return value +} + +const parseWidth = value => { + const parsed = Number(String(value).trim().replace(/%$/, '')) + + return Number.isFinite(parsed) && parsed > 0 ? parsed : null +} + +const normalizeWidths = widths => { + const total = widths.reduce((sum, width) => sum + width, 0) + + if (total <= 0) { + return null + } + + return widths.map(width => width / total * 100) +} + +const parseEntry = entry => { + const separator = entry.indexOf(':') + + if (separator === -1) { + return { + width: parseWidth(entry) + } + } + + return { + pan: entry.slice(0, separator).trim(), + width: parseWidth(entry.slice(separator + 1)) + } +} + +const parseEntries = value => { + return String(value) + .split(',') + .map(entry => entry.trim()) + .filter(Boolean) + .map(parseEntry) +} + +const parseNamedWidths = (entries, visiblePans) => { + const widthsByPan = {} + + for (const { pan, width } of entries) { + if (!pan || panNames.indexOf(pan) === -1 || width === null || widthsByPan[pan]) { + return null + } + + widthsByPan[pan] = width + } + + const widths = visiblePans.map(pan => widthsByPan[pan]) + + if (Object.keys(widthsByPan).length !== visiblePans.length || widths.some(width => width === undefined)) { + return null + } + + return normalizeWidths(widths) +} + +const parseOrderedWidths = (entries, visiblePans) => { + if (entries.length !== visiblePans.length || entries.some(({ pan, width }) => pan || width === null)) { + return null + } + + return normalizeWidths(entries.map(({ width }) => width)) +} + +const formatPercent = value => `${Number(value.toFixed(6))}%` + +export const parsePanWidths = (value, visiblePans) => { + value = normalizeQueryValue(value) + + if (!value || !Array.isArray(visiblePans) || visiblePans.length === 0) { + return null + } + + const entries = parseEntries(value) + + if (entries.length === 0) { + return null + } + + const hasNamedEntries = entries.some(({ pan }) => pan) + + return hasNamedEntries ? + parseNamedWidths(entries, visiblePans) : + parseOrderedWidths(entries, visiblePans) +} + +export const getPanPosition = (visiblePans, pan, widths) => { + const index = visiblePans.indexOf(pan) + + if (!widths || index === -1 || widths.length !== visiblePans.length) { + return null + } + + const left = widths.slice(0, index).reduce((sum, width) => sum + width, 0) + const width = widths[index] + const right = Math.max(0, 100 - left - width) + + return { + left: formatPercent(left), + right: formatPercent(right) + } +} diff --git a/src/views/EditorPage.vue b/src/views/EditorPage.vue index 058100bd..268aba0e 100644 --- a/src/views/EditorPage.vue +++ b/src/views/EditorPage.vue @@ -120,6 +120,12 @@ export default { } }, immediate: true + }, + '$route.query.widths': { + handler(next) { + this.setPanWidths(next) + }, + immediate: true } }, mounted() { @@ -154,7 +160,7 @@ export default { }) }, methods: { - ...mapActions(['setBoilerplate', 'setGist', 'showPans', 'setAutoRun']), + ...mapActions(['setBoilerplate', 'setGist', 'showPans', 'setAutoRun', 'setPanWidths']), isVisible(pan) { return this.visiblePans.indexOf(pan) !== -1 }, diff --git a/test/pan-widths.js b/test/pan-widths.js new file mode 100644 index 00000000..87a393eb --- /dev/null +++ b/test/pan-widths.js @@ -0,0 +1,39 @@ +require('babel-register')({ + presets: [['env', { modules: 'commonjs' }]], + ignore: /node_modules/ +}) + +const assert = require('assert') +const { parsePanWidths, getPanPosition } = require('../src/utils/pan-widths') + +const visiblePans = ['js', 'console', 'output'] + +assert.deepStrictEqual(parsePanWidths('25,25,50', visiblePans), [25, 25, 50]) +assert.deepStrictEqual(parsePanWidths('1,1,2', visiblePans), [25, 25, 50]) +assert.deepStrictEqual(parsePanWidths('25%, 25%, 50%', visiblePans), [25, 25, 50]) +assert.deepStrictEqual(parsePanWidths('output:50,js:25,console:25', visiblePans), [25, 25, 50]) +assert.deepStrictEqual(parsePanWidths('js:1,console:1,output:2', visiblePans), [25, 25, 50]) +assert.deepStrictEqual(parsePanWidths(['25,25,50'], visiblePans), [25, 25, 50]) + +assert.strictEqual(parsePanWidths('', visiblePans), null) +assert.strictEqual(parsePanWidths('25,25', visiblePans), null) +assert.strictEqual(parsePanWidths('25,nope,50', visiblePans), null) +assert.strictEqual(parsePanWidths('25,-1,50', visiblePans), null) +assert.strictEqual(parsePanWidths('js:25,output:75', visiblePans), null) +assert.strictEqual(parsePanWidths('js:25,console:25,output:0', visiblePans), null) +assert.strictEqual(parsePanWidths('js:25,console:25,output:25,html:25', visiblePans), null) +assert.strictEqual(parsePanWidths('missing:25,console:25,output:50', visiblePans), null) + +assert.deepStrictEqual( + getPanPosition(visiblePans, 'js', [25, 25, 50]), + { left: '0%', right: '75%' } +) +assert.deepStrictEqual( + getPanPosition(visiblePans, 'console', [25, 25, 50]), + { left: '25%', right: '50%' } +) +assert.deepStrictEqual( + getPanPosition(visiblePans, 'output', [25, 25, 50]), + { left: '50%', right: '0%' } +) +assert.strictEqual(getPanPosition(['js', 'output'], 'output', [25, 25, 50]), null)