mirror of
https://github.com/MercuryWorkshop/dreamlandjs.git
synced 2025-05-15 15:10:02 -04:00
feat: scoped css
This commit is contained in:
parent
321a71c11e
commit
ab9c880b9c
5 changed files with 141 additions and 39 deletions
|
@ -1,5 +1,7 @@
|
||||||
export const VERSION = '0.0.9'
|
export const DLVERSION = '0.0.9'
|
||||||
|
|
||||||
// We add some extra properties into various objects throughout, better to use symbols and not interfere. this is just a tiny optimization
|
// We add some extra properties into various objects throughout, better to use symbols and not interfere. this is just a tiny optimization
|
||||||
export const [USE_MAPFN, TARGET, PROXY, STEPS, LISTENERS, IF, STATEHOOK] =
|
export const [USE_MAPFN, TARGET, PROXY, STEPS, LISTENERS, IF, STATEHOOK] =
|
||||||
Array.from(Array(7), Symbol)
|
Array.from(Array(7), Symbol)
|
||||||
|
|
||||||
|
export const cssBoundary = 'dl-boundary'
|
||||||
|
|
|
@ -8,7 +8,10 @@ import {
|
||||||
PROXY,
|
PROXY,
|
||||||
STEPS,
|
STEPS,
|
||||||
IF,
|
IF,
|
||||||
|
cssBoundary,
|
||||||
} from './consts'
|
} from './consts'
|
||||||
|
|
||||||
|
|
||||||
// saves a few characters, since document will never change
|
// saves a few characters, since document will never change
|
||||||
let doc = document
|
let doc = document
|
||||||
|
|
||||||
|
@ -309,11 +312,13 @@ export function h(type, props, ...children) {
|
||||||
let elm = type.apply(newthis)
|
let elm = type.apply(newthis)
|
||||||
elm.$ = newthis
|
elm.$ = newthis
|
||||||
newthis.root = elm
|
newthis.root = elm
|
||||||
|
/* FEATURE.CSS.START */
|
||||||
|
let cl = elm.classList
|
||||||
if (newthis.css) {
|
if (newthis.css) {
|
||||||
let cl = elm.classList
|
|
||||||
cl.add(newthis.css)
|
cl.add(newthis.css)
|
||||||
cl.add('self')
|
|
||||||
}
|
}
|
||||||
|
cl.add(cssBoundary)
|
||||||
|
/* FEATURE.CSS.END */
|
||||||
elm.setAttribute('data-component', type.name)
|
elm.setAttribute('data-component', type.name)
|
||||||
if (typeof newthis.mount === 'function') newthis.mount()
|
if (typeof newthis.mount === 'function') newthis.mount()
|
||||||
return elm
|
return elm
|
||||||
|
|
113
src/css.js
113
src/css.js
|
@ -1,31 +1,70 @@
|
||||||
|
import { cssBoundary } from "./consts"
|
||||||
|
|
||||||
const cssmap = {}
|
const cssmap = {}
|
||||||
|
|
||||||
export function css(strings, ...values) {
|
|
||||||
let str = ''
|
|
||||||
for (let f of strings) {
|
|
||||||
str += f + (values.shift() || '')
|
|
||||||
}
|
|
||||||
|
|
||||||
let cached = cssmap[str]
|
|
||||||
if (cached) return cached
|
|
||||||
|
|
||||||
const uid = `dl${Array(5)
|
/* POLYFILL.SCOPE.START */
|
||||||
|
let scopeSupported;
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = '@scope (.test) { :scope { color: red } }';
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
const testElement = document.createElement('div');
|
||||||
|
testElement.className = 'test';
|
||||||
|
document.body.appendChild(testElement);
|
||||||
|
|
||||||
|
const computedColor = getComputedStyle(testElement).color;
|
||||||
|
document.head.removeChild(style);
|
||||||
|
document.body.removeChild(testElement);
|
||||||
|
|
||||||
|
scopeSupported = computedColor == 'rgb(255, 0, 0)'
|
||||||
|
});
|
||||||
|
|
||||||
|
const depth = 50;
|
||||||
|
// polyfills @scope for firefox and older browsers, using a :not selector recursively increasing in depth
|
||||||
|
// depth 50 means that after 50 layers of nesting, switching between an unrelated component and the target component, it will eventually stop applying styles (or let them leak into children)
|
||||||
|
// this is slow. please ask mozilla to implement @scope
|
||||||
|
function polyfill_scope(target) {
|
||||||
|
let boundary = `:not(${target}).${cssBoundary}`
|
||||||
|
let g = (str, i) => `${str} *${i > depth ? "" : `:not(${g(str + " " + ((i % 2 == 0) ? target : boundary), i + 1)})`}`
|
||||||
|
return `:not(${g(boundary, 0)})`
|
||||||
|
}
|
||||||
|
/* POLYFILL.SCOPE.END */
|
||||||
|
|
||||||
|
|
||||||
|
export function genuid() {
|
||||||
|
return `dl${Array(5)
|
||||||
.fill(0)
|
.fill(0)
|
||||||
.map(() => {
|
.map(() => {
|
||||||
return Math.floor(Math.random() * 36).toString(36)
|
return Math.floor(Math.random() * 36).toString(36)
|
||||||
})
|
})
|
||||||
.join('')}`
|
.join('')}`
|
||||||
|
}
|
||||||
|
|
||||||
cssmap[str] = uid
|
const csstag = (scoped) =>
|
||||||
const styleElement = document.createElement('style')
|
function css(strings, ...values) {
|
||||||
document.head.appendChild(styleElement)
|
let str = ''
|
||||||
|
for (let f of strings) {
|
||||||
|
str += f + (values.shift() || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
return genCss(str, scoped)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const css = csstag(false)
|
||||||
|
export const scope = csstag(true)
|
||||||
|
|
||||||
|
|
||||||
|
function parseCombinedCss(str) {
|
||||||
let newstr = ''
|
let newstr = ''
|
||||||
let selfstr = ''
|
let selfstr = ''
|
||||||
|
|
||||||
// compat layer for older browsers. when css nesting stablizes this can be removed
|
// compat layer for older browsers. when css nesting stablizes this can be removed
|
||||||
str += '\n'
|
str += '\n'
|
||||||
for (;;) {
|
for (; ;) {
|
||||||
let [first, ...rest] = str.split('\n')
|
let [first, ...rest] = str.split('\n')
|
||||||
if (first.trim().endsWith('{')) break
|
if (first.trim().endsWith('{')) break
|
||||||
|
|
||||||
|
@ -33,14 +72,52 @@ export function css(strings, ...values) {
|
||||||
str = rest.join('\n')
|
str = rest.join('\n')
|
||||||
if (!str) break
|
if (!str) break
|
||||||
}
|
}
|
||||||
styleElement.textContent = str
|
|
||||||
|
|
||||||
for (const rule of styleElement.sheet.cssRules) {
|
return [newstr, selfstr, str]
|
||||||
rule.selectorText = `.${uid} ${rule.selectorText}`
|
}
|
||||||
newstr += rule.cssText + '\n'
|
|
||||||
|
function genCss(str, scoped) {
|
||||||
|
let cached = cssmap[str]
|
||||||
|
if (cached) return cached
|
||||||
|
|
||||||
|
const uid = genuid();
|
||||||
|
cssmap[str] = uid
|
||||||
|
|
||||||
|
|
||||||
|
const styleElement = document.createElement('style')
|
||||||
|
document.head.appendChild(styleElement)
|
||||||
|
|
||||||
|
if (scoped) {
|
||||||
|
/* POLYFILL.SCOPE.START */
|
||||||
|
if (!scopeSupported) {
|
||||||
|
[newstr, selfstr, str] = parseCombinedCss(str)
|
||||||
|
|
||||||
|
styleElement.textContent = str
|
||||||
|
|
||||||
|
let scoped = polyfill_scope(`.${uid}`, 50);
|
||||||
|
for (const rule of styleElement.sheet.cssRules) {
|
||||||
|
rule.selectorText = `.${uid} ${rule.selectorText}${scoped}`
|
||||||
|
newstr += rule.cssText + '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
styleElement.textContent = `.${uid} {${selfstr}}` + '\n' + newstr
|
||||||
|
return uid
|
||||||
|
}
|
||||||
|
/* POLYFILL.SCOPE.END */
|
||||||
|
|
||||||
|
styleElement.textContent = `@scope (.${uid}) to (:not(.${uid}).dl-boundary *) { :scope { ${str} } }`
|
||||||
|
} else {
|
||||||
|
[newstr, selfstr, str] = parseCombinedCss(str)
|
||||||
|
|
||||||
|
styleElement.textContent = str
|
||||||
|
|
||||||
|
for (const rule of styleElement.sheet.cssRules) {
|
||||||
|
rule.selectorText = `.${uid} ${rule.selectorText}`
|
||||||
|
newstr += rule.cssText + '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
styleElement.textContent = `.${uid} {${selfstr}}` + '\n' + newstr
|
||||||
}
|
}
|
||||||
|
|
||||||
styleElement.textContent = `.${uid} {${selfstr}}` + '\n' + newstr
|
|
||||||
|
|
||||||
return uid
|
return uid
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,49 @@
|
||||||
|
import { assert } from './asserts'
|
||||||
import { h } from './core'
|
import { h } from './core'
|
||||||
|
import { genuid } from './css'
|
||||||
|
|
||||||
export function html(strings, ...values) {
|
export function html(strings, ...values) {
|
||||||
|
// normalize the strings array, it would otherwise give us an object
|
||||||
|
strings = [...strings]
|
||||||
let flattened = ''
|
let flattened = ''
|
||||||
let markers = {}
|
let markers = {}
|
||||||
for (const i in strings) {
|
for (let i = 0; i < strings.length; i++) {
|
||||||
let string = strings[i]
|
let string = strings[i]
|
||||||
let value = values[i]
|
let value = values[i]
|
||||||
|
|
||||||
|
// since self closing tags don't exist in regular html, look for the pattern <tag /> enclosing a function, and replace it with `<tag`
|
||||||
|
let match =
|
||||||
|
values[i] instanceof Function && /^ *\/\>/.exec(strings[i + 1])
|
||||||
|
if (/\< *$/.test(string) && match) {
|
||||||
|
strings[i + 1] = strings[i + 1].substr(
|
||||||
|
match.index + match[0].length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
flattened += string
|
flattened += string
|
||||||
if (i < values.length) {
|
if (i < values.length) {
|
||||||
let dupe = Object.values(markers).findIndex((v) => v == value)
|
let dupe = Object.values(markers).findIndex((v) => v == value)
|
||||||
|
let marker
|
||||||
if (dupe !== -1) {
|
if (dupe !== -1) {
|
||||||
flattened += Object.keys(markers)[dupe]
|
marker = Object.keys(markers)[dupe]
|
||||||
} else {
|
} else {
|
||||||
let marker =
|
marker = genuid()
|
||||||
'm' +
|
|
||||||
Array(16)
|
|
||||||
.fill(0)
|
|
||||||
.map(() => Math.floor(Math.random() * 16).toString(16))
|
|
||||||
.join('')
|
|
||||||
markers[marker] = value
|
markers[marker] = value
|
||||||
flattened += marker
|
}
|
||||||
|
|
||||||
|
flattened += marker
|
||||||
|
|
||||||
|
// close the self closing tag
|
||||||
|
if (match) {
|
||||||
|
flattened += `></${marker}>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let dom = new DOMParser().parseFromString(flattened, 'text/html')
|
let dom = new DOMParser().parseFromString(flattened, 'text/html')
|
||||||
if (dom.body.children.length !== 1)
|
assert(
|
||||||
throw 'html builder needs exactly one child'
|
dom.body.children.length == 1,
|
||||||
|
'html builder needs exactly one child'
|
||||||
|
)
|
||||||
|
|
||||||
function wraph(elm) {
|
function wraph(elm) {
|
||||||
let nodename = elm.nodeName.toLowerCase()
|
let nodename = elm.nodeName.toLowerCase()
|
||||||
|
@ -41,7 +58,7 @@ export function html(strings, ...values) {
|
||||||
if (!text) break
|
if (!text) break
|
||||||
if (!text.includes(marker)) continue
|
if (!text.includes(marker)) continue
|
||||||
let before
|
let before
|
||||||
;[before, text] = text.split(marker)
|
;[before, text] = text.split(marker)
|
||||||
children = [
|
children = [
|
||||||
...children.slice(0, i),
|
...children.slice(0, i),
|
||||||
before,
|
before,
|
||||||
|
|
13
src/main.js
13
src/main.js
|
@ -1,26 +1,27 @@
|
||||||
import { VERSION } from './consts'
|
import { DLVERSION } from './consts'
|
||||||
export { VERSION as DLVERSION }
|
|
||||||
|
export { DLVERSION }
|
||||||
|
|
||||||
export * from './core'
|
export * from './core'
|
||||||
// $state was named differently in older versions
|
// $state was named differently in older versions
|
||||||
export { $state as stateful } from './core'
|
export { $state as stateful } from './core'
|
||||||
|
|
||||||
/* FEATURE.CSS.START */
|
/* FEATURE.CSS.START */
|
||||||
export * from './css'
|
export { css, scope } from './css'
|
||||||
/* FEATURE.CSS.END */
|
/* FEATURE.CSS.END */
|
||||||
|
|
||||||
/* FEATURE.JSXLITERALS.START */
|
/* FEATURE.JSXLITERALS.START */
|
||||||
export * from './jsxLiterals'
|
export { html } from './jsxLiterals'
|
||||||
/* FEATURE.JSXLITERALS.END */
|
/* FEATURE.JSXLITERALS.END */
|
||||||
|
|
||||||
/* FEATURE.STORES.START */
|
/* FEATURE.STORES.START */
|
||||||
export * from './stores'
|
export { $store } from './stores'
|
||||||
/* FEATURE.STORES.END */
|
/* FEATURE.STORES.END */
|
||||||
|
|
||||||
/* DEV.START */
|
/* DEV.START */
|
||||||
import { log } from './asserts'
|
import { log } from './asserts'
|
||||||
|
|
||||||
log('Version: ' + VERSION)
|
log('Version: ' + DLVERSION)
|
||||||
console.warn(
|
console.warn(
|
||||||
'This is a DEVELOPER build of dreamland.js. It is not suitable for production use.'
|
'This is a DEVELOPER build of dreamland.js. It is not suitable for production use.'
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue