diff --git a/README.md b/README.md
index cb41cf7..e8daa5a 100644
--- a/README.md
+++ b/README.md
@@ -96,6 +96,32 @@ Then specify the language with the `mode` attribute
```
+### Text-marking
+
+Can be used to mark a range of text with a specific CSS class name.
+
+```html
+
+
+
+
+```
+
+### Gutter markers
+
+Can be used to add extra gutters (beyond of the line number gutter). Lines starts from zero.
+
+```html
+
+
+
+ ●
+
+
+```
+
### Theming
Theming requires importing an editor theme stylesheet within `wc-codemirror` tag. You can import few themes this way and switch them with the `theme` attribute.
diff --git a/src/wc-codemirror.js b/src/wc-codemirror.js
index 6f860be..429e899 100644
--- a/src/wc-codemirror.js
+++ b/src/wc-codemirror.js
@@ -46,28 +46,39 @@ export class WCCodeMirror extends HTMLElement {
get value () { return this.editor.getValue() }
set value (value) {
- this.setValue(value)
+ if (this.__initialized) {
+ this.setValueForced(value)
+ } else {
+ // Save to pre init
+ this.__preInitValue = value
+ }
}
constructor () {
super()
+
+ this.setupMutationObserver()
+ this.attachShadow({ mode: 'open' })
+
+ // Create template
+ const template = document.createElement('template')
+ const stylesheet = document.createElement('style')
+ stylesheet.innerHTML = CODE_MIRROR_CSS_CONTENT
+ template.innerHTML = WCCodeMirror.template()
+ this.shadowRoot.appendChild(stylesheet)
+ this.shadowRoot.appendChild(template.content.cloneNode(true))
+
+ this.__textMarks = []
+ this.__gutters = []
this.__initialized = false
this.__element = null
this.editor = null
}
async connectedCallback () {
- // Create template
- const shadow = this.attachShadow({ mode: 'open' })
- const template = document.createElement('template')
- const stylesheet = document.createElement("style")
- stylesheet.innerHTML = CODE_MIRROR_CSS_CONTENT
- template.innerHTML = WCCodeMirror.template()
- shadow.appendChild(stylesheet)
- shadow.appendChild(template.content.cloneNode(true))
// Initialization
this.style.display = 'block'
- this.__element = shadow.querySelector('textarea')
+ this.__element = this.shadowRoot.querySelector('textarea')
const mode = this.hasAttribute('mode') ? this.getAttribute('mode') : 'null'
const theme = this.hasAttribute('theme') ? this.getAttribute('theme') : 'default'
@@ -76,17 +87,6 @@ export class WCCodeMirror extends HTMLElement {
if (readOnly === '') readOnly = true
else if (readOnly !== 'nocursor') readOnly = false
- this.refreshStyles()
-
- let content = ''
- const innerScriptTag = this.querySelector('script')
- if (innerScriptTag) {
- if (innerScriptTag.getAttribute('type') === 'wc-content') {
- content = WCCodeMirror.dedentText(innerScriptTag.innerHTML)
- content = content.replace(/<(\/?script)(.*?)>/g, '<$1$2>')
- }
- }
-
let viewportMargin = CodeMirror.defaults.viewportMargin
if (this.hasAttribute('viewport-margin')) {
const viewportMarginAttr = this.getAttribute('viewport-margin').toLowerCase()
@@ -101,21 +101,32 @@ export class WCCodeMirror extends HTMLElement {
viewportMargin
})
+ this.refreshStyleLinks()
+ this.refrestWcContent()
+ this.setupEvents()
+
if (this.hasAttribute('src')) {
- this.setSrc(this.getAttribute('src'))
- } else {
- // delay until editor initializes
- await new Promise(resolve => setTimeout(resolve, 50))
- this.value = content
+ this.setSrc()
}
+ // delay until editor initializes
+ await new Promise(resolve => setTimeout(resolve, 50))
this.__initialized = true
+
+ if (this.__preInitValue !== undefined) {
+ this.setValueForced(this.__preInitValue)
+ }
+
+ // This should be invoked after text set
+ this.refreshMarkText()
+ this.refreshGutters()
}
disconnectedCallback () {
this.editor && this.editor.toTextArea()
this.editor = null
this.__initialized = false
+ this.__observer.disconnect()
}
async setSrc () {
@@ -124,7 +135,10 @@ export class WCCodeMirror extends HTMLElement {
this.value = contents
}
- async setValue (value) {
+ /**
+ * Set value without initialization check
+ */
+ async setValueForced (value) {
this.editor.swapDoc(CodeMirror.Doc(value, this.getAttribute('mode')))
this.editor.refresh()
}
@@ -134,7 +148,7 @@ export class WCCodeMirror extends HTMLElement {
return response.text()
}
- refreshStyles () {
+ refreshStyleLinks () {
// Remove all element in shadow root
Array.from(this.shadowRoot.children).forEach(element => {
if (element.tagName === 'LINK' && element.getAttribute('rel') === 'stylesheet') {
@@ -149,6 +163,190 @@ export class WCCodeMirror extends HTMLElement {
})
}
+ refrestWcContent () {
+ const innerScriptTag = this.querySelector('script')
+ if (innerScriptTag) {
+ if (innerScriptTag.getAttribute('type') === 'wc-content') {
+ const data = WCCodeMirror.dedentText(innerScriptTag.innerHTML)
+ this.value = data.replace(/<(\/?script)(.*?)>/g, '<$1$2>')
+ }
+ }
+ }
+
+ refreshMarkText () {
+ // Remove all old marks
+ this.__textMarks.forEach(element => {
+ element.clear()
+ })
+ this.__textMarks = Array.from(this.children)
+ .filter(element => element.tagName === 'MARK-TEXT')
+ .map(element => {
+ try {
+ const fromLine = parseInt(element.getAttribute('from-line'))
+ const fromChar = parseInt(element.getAttribute('from-char'))
+ const toLine = parseInt(element.getAttribute('to-line'))
+ const toChar = parseInt(element.getAttribute('to-char'))
+ const options = JSON.parse(element.getAttribute('options').replace(/'/g, '"'))
+ const from = { line: fromLine, ch: fromChar }
+ const to = { line: toLine, ch: toChar }
+ return this.editor.markText(from, to, options)
+ } catch (error) {
+ console.error(error)
+ // Return ermpty descriptor
+ return { clear: () => {} }
+ }
+ })
+ }
+
+ refreshGutters () {
+ // Remove all gutters
+ this.__gutters.forEach(gutter => this.editor.clearGutter(gutter.name))
+ this.__gutters = Array.from(this.children)
+ .filter((g) => g.tagName === 'GUTTERS' && g.hasAttribute('name'))
+ .map((g) => {
+ return {
+ name: g.getAttribute('name'),
+ lines: Array.from(g.children)
+ .filter(e => e.tagName === 'GUTTER' && e.children.length > 0)
+ .map((e) => {
+ const line = parseInt(e.getAttribute('line'))
+ const firstChild = e.children[0]
+ return { line, marker: firstChild }
+ })
+ }
+ })
+ this.editor.setOption('gutters', this.__gutters.map((g) => g.name))
+ // Setup markers
+ this.__gutters.forEach((g) => {
+ g.lines.forEach((e) => {
+ this.editor.setGutterMarker(e.line, g.name, e.marker.cloneNode(true))
+ })
+ })
+ }
+
+ setupMutationObserver () {
+ const observerConfig = {
+ childList: true,
+ characterData: true,
+ subtree: true,
+ attributes: true
+ }
+
+ const nodeListContainsTag = (nodeList, tag) => {
+ const checkThatTag = (e) => e.tagName === tag
+ const removed = Array.from(nodeList)
+ return removed.some(checkThatTag)
+ }
+
+ const mutContainsRemovedTag = (tag) => (record) => {
+ return nodeListContainsTag(record.removedNodes, tag)
+ }
+
+ const mutContainsAddedTag = (tag) => (record) => {
+ return nodeListContainsTag(record.addedNodes, tag)
+ }
+
+ const mutTargetHierarchyContainsTag = (tag) => (record) => {
+ let tagetMatched = false
+ for (let t = record.target; t !== null && t !== this; t = t.parentNode) {
+ if (t.tagName === tag) {
+ tagetMatched = true
+ break
+ }
+ }
+ return tagetMatched
+ }
+
+ const mutContainsTag = (tag) => {
+ const containsAdded = mutContainsAddedTag(tag)
+ const containsRemoved = mutContainsRemovedTag(tag)
+ const matchTarget = mutTargetHierarchyContainsTag(tag)
+
+ return (record) => matchTarget(record) || containsAdded(record) || containsRemoved(record)
+ }
+
+ const mutContainsLink = mutContainsTag('LINK')
+ const mutContainsMarkText = mutContainsTag('MARK-TEXT')
+ const mutContainsGutters = mutContainsTag('GUTTERS')
+ const mutContainsGutter = mutContainsTag('GUTTER')
+ const mutContainsRemovedScript = mutContainsRemovedTag('SCRIPT')
+ const mutContainsAddedScript = mutContainsAddedTag('SCRIPT')
+ const mutTargetHierarchyContainsScript = mutTargetHierarchyContainsTag('SCRIPT')
+
+ this.__observer = new MutationObserver((mutationsList, observer) => {
+ let doRefreshMarks = false
+ let doRefreshGutters = false
+
+ mutationsList.forEach((record) => {
+ if (mutContainsLink(record)) {
+ this.refreshStyleLinks()
+ }
+ if (mutContainsRemovedScript(record)) {
+ this.value = ''
+ doRefreshMarks = true
+ doRefreshGutters = true
+ }
+ if (mutContainsAddedScript(record) || mutTargetHierarchyContainsScript(record)) {
+ this.refrestWcContent()
+ doRefreshMarks = true
+ doRefreshGutters = true
+ }
+ if (mutContainsGutters(record) || mutContainsGutter(record)) {
+ doRefreshGutters = true
+ }
+ if (mutContainsMarkText(record)) {
+ doRefreshMarks = true
+ }
+ })
+
+ // Perform refresh
+ if (doRefreshMarks) {
+ this.refreshMarkText()
+ }
+ if (doRefreshGutters) {
+ this.refreshGutters()
+ }
+ })
+
+ this.__observer.observe(this, observerConfig)
+ }
+
+ setupEvent (type, ...argNames) {
+ CodeMirror.on(this.editor, type, (...args) => {
+ const detail = {}
+ args.shift() // Remove instance
+ args.forEach((arg, i) => {
+ detail[argNames[i]] = arg
+ })
+ const initDict = { bubbles: true, detail }
+ const event = new CustomEvent(type, initDict)
+ this.dispatchEvent(event)
+ })
+ }
+
+ setupEvents () {
+ this.setupEvent('change', 'changeObj')
+ this.setupEvent('changes', 'changes')
+ this.setupEvent('beforeChange', 'changeObj')
+ this.setupEvent('cursorActivity')
+ this.setupEvent('keyHandled', 'name', 'event')
+ this.setupEvent('inputRead', 'changeObj')
+ this.setupEvent('electricInput', 'line')
+ this.setupEvent('beforeSelectionChange', 'obj')
+ this.setupEvent('viewportChange', 'from', 'to')
+ this.setupEvent('swapDoc', 'oldDoc')
+ this.setupEvent('gutterClick', 'line', 'gutter', 'event')
+ this.setupEvent('gutterContextMenu', 'line', 'gutter', 'event')
+ this.setupEvent('focus', 'event')
+ this.setupEvent('blur', 'event')
+ this.setupEvent('scroll')
+ this.setupEvent('refresh')
+ this.setupEvent('optionChange', 'option')
+ this.setupEvent('scrollCursorIntoView', 'event')
+ this.setupEvent('update')
+ this.setupEvent('renderLine', 'line', 'element')
+ }
+
static template () {
return `