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)