diff --git a/src/consts.js b/src/consts.js
index 43d3a17..0f3525e 100644
--- a/src/consts.js
+++ b/src/consts.js
@@ -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
export const [USE_MAPFN, TARGET, PROXY, STEPS, LISTENERS, IF, STATEHOOK] =
Array.from(Array(7), Symbol)
+
+export const cssBoundary = 'dl-boundary'
diff --git a/src/core.js b/src/core.js
index 9e79c52..0a5d962 100644
--- a/src/core.js
+++ b/src/core.js
@@ -8,7 +8,10 @@ import {
PROXY,
STEPS,
IF,
+ cssBoundary,
} from './consts'
+
+
// saves a few characters, since document will never change
let doc = document
@@ -309,11 +312,13 @@ export function h(type, props, ...children) {
let elm = type.apply(newthis)
elm.$ = newthis
newthis.root = elm
+ /* FEATURE.CSS.START */
+ let cl = elm.classList
if (newthis.css) {
- let cl = elm.classList
cl.add(newthis.css)
- cl.add('self')
}
+ cl.add(cssBoundary)
+ /* FEATURE.CSS.END */
elm.setAttribute('data-component', type.name)
if (typeof newthis.mount === 'function') newthis.mount()
return elm
diff --git a/src/css.js b/src/css.js
index a61c4f8..5205aac 100644
--- a/src/css.js
+++ b/src/css.js
@@ -1,31 +1,70 @@
+import { cssBoundary } from "./consts"
+
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)
.map(() => {
return Math.floor(Math.random() * 36).toString(36)
})
.join('')}`
+}
- cssmap[str] = uid
- const styleElement = document.createElement('style')
- document.head.appendChild(styleElement)
+const csstag = (scoped) =>
+ function css(strings, ...values) {
+ 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 selfstr = ''
// compat layer for older browsers. when css nesting stablizes this can be removed
str += '\n'
- for (;;) {
+ for (; ;) {
let [first, ...rest] = str.split('\n')
if (first.trim().endsWith('{')) break
@@ -33,14 +72,52 @@ export function css(strings, ...values) {
str = rest.join('\n')
if (!str) break
}
- styleElement.textContent = str
- for (const rule of styleElement.sheet.cssRules) {
- rule.selectorText = `.${uid} ${rule.selectorText}`
- newstr += rule.cssText + '\n'
+ return [newstr, selfstr, str]
+}
+
+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
}
diff --git a/src/jsxLiterals.js b/src/jsxLiterals.js
index ee022e5..a6706a9 100644
--- a/src/jsxLiterals.js
+++ b/src/jsxLiterals.js
@@ -1,32 +1,49 @@
+import { assert } from './asserts'
import { h } from './core'
+import { genuid } from './css'
export function html(strings, ...values) {
+ // normalize the strings array, it would otherwise give us an object
+ strings = [...strings]
let flattened = ''
let markers = {}
- for (const i in strings) {
+ for (let i = 0; i < strings.length; i++) {
let string = strings[i]
let value = values[i]
+ // since self closing tags don't exist in regular html, look for the pattern enclosing a function, and replace it with `/.exec(strings[i + 1])
+ if (/\< *$/.test(string) && match) {
+ strings[i + 1] = strings[i + 1].substr(
+ match.index + match[0].length
+ )
+ }
+
flattened += string
if (i < values.length) {
let dupe = Object.values(markers).findIndex((v) => v == value)
+ let marker
if (dupe !== -1) {
- flattened += Object.keys(markers)[dupe]
+ marker = Object.keys(markers)[dupe]
} else {
- let marker =
- 'm' +
- Array(16)
- .fill(0)
- .map(() => Math.floor(Math.random() * 16).toString(16))
- .join('')
+ marker = genuid()
markers[marker] = value
- flattened += marker
+ }
+
+ flattened += marker
+
+ // close the self closing tag
+ if (match) {
+ flattened += `>${marker}>`
}
}
}
let dom = new DOMParser().parseFromString(flattened, 'text/html')
- if (dom.body.children.length !== 1)
- throw 'html builder needs exactly one child'
+ assert(
+ dom.body.children.length == 1,
+ 'html builder needs exactly one child'
+ )
function wraph(elm) {
let nodename = elm.nodeName.toLowerCase()
@@ -41,7 +58,7 @@ export function html(strings, ...values) {
if (!text) break
if (!text.includes(marker)) continue
let before
- ;[before, text] = text.split(marker)
+ ;[before, text] = text.split(marker)
children = [
...children.slice(0, i),
before,
diff --git a/src/main.js b/src/main.js
index 139c344..d7f476f 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,26 +1,27 @@
-import { VERSION } from './consts'
-export { VERSION as DLVERSION }
+import { DLVERSION } from './consts'
+
+export { DLVERSION }
export * from './core'
// $state was named differently in older versions
export { $state as stateful } from './core'
/* FEATURE.CSS.START */
-export * from './css'
+export { css, scope } from './css'
/* FEATURE.CSS.END */
/* FEATURE.JSXLITERALS.START */
-export * from './jsxLiterals'
+export { html } from './jsxLiterals'
/* FEATURE.JSXLITERALS.END */
/* FEATURE.STORES.START */
-export * from './stores'
+export { $store } from './stores'
/* FEATURE.STORES.END */
/* DEV.START */
import { log } from './asserts'
-log('Version: ' + VERSION)
+log('Version: ' + DLVERSION)
console.warn(
'This is a DEVELOPER build of dreamland.js. It is not suitable for production use.'
)