export class ZeroMd extends HTMLElement {
get src () { return this.getAttribute('src') }
set src (val) { this.reflect('src', val) }
get manualRender () { return this.hasAttribute('manual-render') }
set manualRender (val) { this.reflect('manual-render', val) }
reflect (name, val) {
if (val === false) {
this.removeAttribute(name)
} else {
this.setAttribute(name, val === true ? '' : val)
}
}
static get observedAttributes () {
return ['src']
}
attributeChangedCallback (name, old, val) {
if (name === 'src' && this.connected && !this.manualRender && val !== old) {
this.render()
}
}
constructor (defaults) {
super()
this.version = '$VERSION'
this.config = {
markedUrl: 'https://cdn.jsdelivr.net/gh/markedjs/marked@2/marked.min.js',
prismUrl: [
['https://cdn.jsdelivr.net/gh/PrismJS/prism@1/prism.min.js', 'data-manual'],
'https://cdn.jsdelivr.net/gh/PrismJS/prism@1/plugins/autoloader/prism-autoloader.min.js'
],
cssUrls: [
'https://cdn.jsdelivr.net/gh/sindresorhus/github-markdown-css@4/github-markdown.min.css',
'https://cdn.jsdelivr.net/gh/PrismJS/prism@1/themes/prism.min.css'
],
hostCss: ':host{display:block;position:relative;contain:content;}:host([hidden]){display:none;}',
...defaults,
...window.ZeroMdConfig
}
this.cache = {}
this.root = this.hasAttribute('no-shadow') ? this : this.attachShadow({ mode: 'open' })
if (!this.constructor.ready) {
this.constructor.ready = Promise.all([
!!window.marked || this.loadScript(this.config.markedUrl),
!!window.Prism || this.loadScript(this.config.prismUrl)
])
}
this.clicked = this.clicked.bind(this)
if (!this.manualRender) {
// Scroll to hash id after first render. However, `history.scrollRestoration` inteferes with this on refresh.
// It's much better to use a `setTimeout` rather than to alter the browser's behaviour.
this.render().then(() => setTimeout(() => this.goto(location.hash), 250))
}
this.observer = new MutationObserver(async () => {
this.observeChanges()
if (!this.manualRender) {
await this.render()
}
})
this.observeChanges()
}
connectedCallback () {
this.connected = true
this.fire('zero-md-connected', {}, { bubbles: false, composed: false })
this.waitForReady().then(() => {
this.fire('zero-md-ready')
})
if (this.shadowRoot) {
this.shadowRoot.addEventListener('click', this.clicked)
}
}
disconnectedCallback () {
this.connected = false
if (this.shadowRoot) {
this.shadowRoot.removeEventListener('click', this.clicked)
}
}
waitForReady () {
const ready = this.connected || new Promise(resolve => {
this.addEventListener('zero-md-connected', function handler () {
this.removeEventListener('zero-md-connected', handler)
resolve()
})
})
return Promise.all([this.constructor.ready, ready])
}
fire (name, detail = {}, opts = { bubbles: true, composed: true }) {
if (detail.msg) {
console.warn(detail.msg)
}
this.dispatchEvent(new CustomEvent(name, {
detail: { node: this, ...detail },
...opts
}))
}
tick () {
return new Promise(resolve => requestAnimationFrame(resolve))
}
// Coerce anything into an array
arrify (any) {
return any ? (Array.isArray(any) ? any : [any]) : []
}
// Promisify an element's onload callback
onload (node) {
return new Promise((resolve, reject) => {
node.onload = resolve
node.onerror = err => reject(err.path ? err.path[0] : err.composedPath()[0])
})
}
// Load a url or load (in order) an array of urls via