Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion demo/assets/Button.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<template>
<div class="hello">
<h1>Colored Text</h1>
<button>{{ msg }}</button>
</div>
</template>
Expand All @@ -14,8 +15,9 @@ export default {
};
</script>

<style scoped>
<style>
.hello {
text-align: center;
color: #900;
}
</style>
3 changes: 1 addition & 2 deletions demo/assets/input.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
new Vue({
template: `
<div>
<input v-model="value" type="checkbox" :name="cname">
<input v-model="value" type="checkbox">
<h1 v-if="value">I am checked</h1>
</div>`,
data() {
return {
cname: "myCheck",
value: false
};
}
Expand Down
40 changes: 14 additions & 26 deletions src/Preview.vue
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
<template>
<div>
<div style="color:red" v-if="error">{{this.error}}</div>
<div :id="scope"/>
<VuePreview :id="scope"/>
</div>
</template>

<script>
import Vue from "vue";
import { transform } from "buble";
import compileCode, { isCodeVueSfc } from "./utils/compileCode";
import getVars from "./utils/getVars";
import getVueConfigObject from "./utils/getVueConfigObject";
import styleScoper from "./utils/styleScoper";
import addScopedStyle from "./utils/addScopedStyle";

export default {
name: "VueLivePreviewComponent",
components: {},
props: {
code: {
type: String,
required: true
},
scoped: {
type: Boolean,
default: true
},
components: {
type: Object,
default: () => {}
Expand All @@ -35,7 +31,7 @@ export default {
error: false
};
},
mounted() {
beforeMount() {
this.renderComponent(this.code.trim());
},
methods: {
Expand All @@ -50,11 +46,11 @@ export default {
this.error = e.message;
},
renderComponent(code) {
let data = {},
script,
style,
template;
let data = {};
let listVars = [];
let script;
let style;
let template;
try {
const renderedComponent = compileCode(code);
style = renderedComponent.style;
Expand Down Expand Up @@ -89,22 +85,14 @@ export default {
}

data.components = this.components;

// eslint-disable-next-line no-new
const vueInstance = new Vue({
el: `#${this.scope}`,
render: createElement => createElement(data)
});

// Add the scoped style if there is any
if (style) {
vueInstance.$el.setAttribute(`data-${this.scope}`, true);
const styleContainer = document.createElement("div");
styleContainer.innerHTML = style;
styleContainer.firstChild.id = `data-${this.scope}`;
vueInstance.$el.appendChild(styleContainer.firstChild);
// To add the scope id attribute to each item in the html
// this way when we add the scoped style sheet it will be aplied
data._scopeId = `data-${this.scope}`;
addScopedStyle(style, this.scope);
}
styleScoper();

this.$options.components.VuePreview = data;
}
}
};
Expand Down
14 changes: 14 additions & 0 deletions src/utils/addScopedStyle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { scoper } from "./styleScoper";

export default function addScopedStyle(css, suffix) {
const head = document.head || document.getElementsByTagName("head")[0];
const newstyle = document.createElement("style");
newstyle.dataset.cssscoper = "true";
const csses = scoper(css, `[data-${suffix}]`);
if (newstyle.styleSheet) {
newstyle.styleSheet.cssText = csses;
} else {
newstyle.appendChild(document.createTextNode(csses));
}
head.appendChild(newstyle);
}
84 changes: 42 additions & 42 deletions src/utils/normalizeSfcComponent.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,55 @@
import { parseComponent } from 'vue-template-compiler'
import walkes from 'walkes'
import transformOneImport from './transformOneImport'
import getAst from './getAst'
import { parseComponent } from "vue-template-compiler";
import walkes from "walkes";
import transformOneImport from "./transformOneImport";
import getAst from "./getAst";

const buildStyles = function(styles) {
let _styles = ''
let _styles = "";
if (styles) {
styles.forEach(it => {
if (it.content) {
_styles += it.content
_styles += it.content;
}
})
});
}
if (_styles !== '') {
return `<style scoped>${_styles.trim()}</style>`
if (_styles !== "") {
return _styles.trim();
}
return undefined
}
return undefined;
};

function getSingleFileComponentParts(code) {
const parts = parseComponent(code)
const parts = parseComponent(code);
if (parts.script)
parts.script.content = parts.script.content.replace(
/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm,
'$1'
)
return parts
"$1"
);
return parts;
}

function injectTemplateAndParseExport(parts) {
const templateString = parts.template.content.replace(/`/g, '\\`')
const templateString = parts.template.content.replace(/`/g, "\\`");

if (!parts.script) return `{\ntemplate: \`${templateString}\` }`
if (!parts.script) return `{\ntemplate: \`${templateString}\` }`;

let code = parts.script.content
let preprocessing = ''
let startIndex = -1
let endIndex = -1
let offset = 0
let code = parts.script.content;
let preprocessing = "";
let startIndex = -1;
let endIndex = -1;
let offset = 0;
walkes(getAst(code), {
// export const MyComponent = {}
ExportNamedDeclaration(node) {
preprocessing = code.slice(0, node.start + offset)
startIndex = node.declaration.declarations[0].init.start + offset
endIndex = node.declaration.declarations[0].init.end + offset
preprocessing = code.slice(0, node.start + offset);
startIndex = node.declaration.declarations[0].init.start + offset;
endIndex = node.declaration.declarations[0].init.end + offset;
},
// export default {}
ExportDefaultDeclaration(node) {
preprocessing = code.slice(0, node.start + offset)
startIndex = node.declaration.start + offset
endIndex = node.declaration.end + offset
preprocessing = code.slice(0, node.start + offset);
startIndex = node.declaration.start + offset;
endIndex = node.declaration.end + offset;
},
// module.exports = {}
AssignmentExpression(node) {
Expand All @@ -59,26 +59,26 @@ function injectTemplateAndParseExport(parts) {
/module/.test(node.left.object.name) &&
/exports/.test(node.left.property.name))
) {
preprocessing = code.slice(0, node.start + offset)
startIndex = node.right.start + offset
endIndex = node.right.end + offset
preprocessing = code.slice(0, node.start + offset);
startIndex = node.right.start + offset;
endIndex = node.right.end + offset;
}
},
ImportDeclaration(node) {
const ret = transformOneImport(node, code, offset)
offset = ret.offset
code = ret.code
const ret = transformOneImport(node, code, offset);
offset = ret.offset;
code = ret.code;
}
})
});
if (startIndex === -1) {
throw new Error('Failed to parse single file component: ' + code)
throw new Error("Failed to parse single file component: " + code);
}
let right = code.slice(startIndex + 1, endIndex - 1)
let right = code.slice(startIndex + 1, endIndex - 1);
return {
preprocessing,
component: `{\n template: \`${templateString}\`,\n ${right}}`,
postprocessing: code.slice(endIndex)
}
};
}

/**
Expand All @@ -87,14 +87,14 @@ function injectTemplateAndParseExport(parts) {
* transformed into requires
*/
export default function normalizeSfcComponent(code) {
const parts = getSingleFileComponentParts(code)
const extractedComponent = injectTemplateAndParseExport(parts)
const parts = getSingleFileComponentParts(code);
const extractedComponent = injectTemplateAndParseExport(parts);
return {
component: [
extractedComponent.preprocessing,
`new Vue(${extractedComponent.component});`,
extractedComponent.postprocessing
].join('\n'),
].join("\n"),
style: buildStyles(parts.styles)
}
};
}
118 changes: 37 additions & 81 deletions src/utils/styleScoper.js
Original file line number Diff line number Diff line change
@@ -1,87 +1,43 @@
/* eslint-disable no-control-regex */

// used to make CSS selectors remain scoped properly
function init() {
const style = document.createElement('style')
style.appendChild(document.createTextNode(''))
document.head.appendChild(style)
style.sheet.insertRule('body { visibility: hidden; }', 0)
}

export function scoper(css, suffix) {
const re = /([^\r\n,{}]+)(,(?=[^}]*{)|s*{)/g

// `after` is going to contain eithe a comma or an opening curly bracket
css = css.replace(re, function(full, selector, after) {
// if non-rule delimiter
if (selector.match(/^\s*(@media|@keyframes|to|from|@font-face)/)) {
return selector + after
}

// deal with :scope pseudo selectors
if (selector.match(/:scope/)) {
selector = selector.replace(/([^\s]*):scope/, function(full, cutSelector) {
if (cutSelector === '') {
return '> *'
}
return '> ' + cutSelector
})
}

// deal with other pseudo selectors
let pseudo = ''
if (selector.match(/:/)) {
const parts = selector.split(/:/)
selector = parts[0]
pseudo = ':' + parts[1]
}

selector = selector.trim() + ' '
selector = selector.replace(/ /g, suffix + pseudo + ' ')

return selector + after
})

return css
}

function process() {
const styles = document.body.querySelectorAll('style[scoped]')

if (styles.length === 0) {
document.getElementsByTagName('body')[0].style.visibility = 'visible'
return
}

const head = document.head || document.getElementsByTagName('head')[0]
const newstyle = document.createElement('style')
newstyle.dataset.cssscoper = 'true'
let csses = ''

let idx
for (idx = 0; idx < styles.length; idx++) {
const style = styles[idx]
const moduleId = style.id
const css = style.innerHTML

if (css && style.parentElement.nodeName !== 'BODY') {
const suffix = '[' + moduleId + ']'
style.parentNode.removeChild(style)

csses = csses + scoper(css, suffix)
}
}

if (newstyle.styleSheet) {
newstyle.styleSheet.cssText = csses
} else {
newstyle.appendChild(document.createTextNode(csses))
}
head.appendChild(newstyle)

document.getElementsByTagName('body')[0].style.visibility = 'visible'
const re = /([^\r\n,{}]+)(,(?=[^}]*{)|s*{)/g;

// `after` is going to contain eithe a comma or an opening curly bracket
css = css.replace(re, function(full, selector, after) {
// if non-rule delimiter
if (selector.match(/^\s*(@media|@keyframes|to|from|@font-face)/)) {
return selector + after;
}

// deal with :scope pseudo selectors
if (selector.match(/:scope/)) {
selector = selector.replace(/([^\s]*):scope/, function(
full,
cutSelector
) {
if (cutSelector === "") {
return "> *";
}
return "> " + cutSelector;
});
}

// deal with other pseudo selectors
let pseudo = "";
if (selector.match(/:/)) {
const parts = selector.split(/:/);
selector = parts[0];
pseudo = ":" + parts[1];
}

selector = selector.trim() + " ";
selector = selector.replace(/ /g, suffix + pseudo + " ");

return selector + after;
});

return css;
}

init()

export default process