Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

close #35: add the way to contol text marks from DOM #36

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,19 @@ Then specify the language with the `mode` attribute
<wc-codemirror mode="javascript"></wc-codemirror>
```

### Text-marking

Can be used to mark a range of text with a specific CSS class name.

```html
<wc-codemirror>
<script type="wc-content">
Hello world
</script>
<mark-text from-line="1" from-char="7" to-line="1" to-char="11" options="{ 'css': 'color: red', 'title': 'cow' }" />
</wc-codemirror>
```

### 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.
Expand Down
143 changes: 123 additions & 20 deletions src/wc-codemirror.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,78 @@ 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()

const observerConfig = {
childList: true,
characterData: true,
subtree: 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 mutContainsTag = (tag) => {
const containsAdded = mutContainsAddedTag(tag)
const containsRemoved = mutContainsRemovedTag(tag)
return (record) => containsAdded(record) || containsRemoved(record)
}

const mutContainsLink = mutContainsTag('LINK')
const mutContainsMarkText = mutContainsTag('MARK-TEXT')
const mutContainsRemovedScript = mutContainsRemovedTag('SCRIPT')
const mutContainsAddedScript = mutContainsAddedTag('SCRIPT')

this.__observer = new MutationObserver((mutationsList, observer) => {
let doRefreshMarks = false
mutationsList.forEach((record) => {
if (record.type === 'childList') {
if (mutContainsLink(record)) {
this.refreshStyleLinks()
}
if (mutContainsRemovedScript(record)) {
this.value = ''
}
if (mutContainsAddedScript(record)) {
this.refrestWcContent()
}
if (mutContainsMarkText(record)) {
doRefreshMarks = true
}
} else if (record.type === 'characterData') {
// Text data had been chaged. It's a reason to
// check wc-content
this.refrestWcContent()
}
})
if (doRefreshMarks) {
this.refreshMarkText()
}
})

this.__observer.observe(this, observerConfig)

this.__textMarks = []
this.__initialized = false
this.__element = null
this.editor = null
Expand All @@ -60,7 +127,7 @@ export class WCCodeMirror extends HTMLElement {
// Create template
const shadow = this.attachShadow({ mode: 'open' })
const template = document.createElement('template')
const stylesheet = document.createElement("style")
const stylesheet = document.createElement('style')
stylesheet.innerHTML = CODE_MIRROR_CSS_CONTENT
template.innerHTML = WCCodeMirror.template()
shadow.appendChild(stylesheet)
Expand All @@ -76,17 +143,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(/&lt;(\/?script)(.*?)&gt;/g, '<$1$2>')
}
}

let viewportMargin = CodeMirror.defaults.viewportMargin
if (this.hasAttribute('viewport-margin')) {
const viewportMarginAttr = this.getAttribute('viewport-margin').toLowerCase()
Expand All @@ -101,21 +157,30 @@ export class WCCodeMirror extends HTMLElement {
viewportMargin
})

this.refreshStyleLinks()
this.refrestWcContent()

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()
}

disconnectedCallback () {
this.editor && this.editor.toTextArea()
this.editor = null
this.__initialized = false
this.__observer.disconnect()
}

async setSrc () {
Expand All @@ -124,7 +189,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()
}
Expand All @@ -134,7 +202,7 @@ export class WCCodeMirror extends HTMLElement {
return response.text()
}

refreshStyles () {
refreshStyleLinks () {
// Remove all <link> element in shadow root
Array.from(this.shadowRoot.children).forEach(element => {
if (element.tagName === 'LINK' && element.getAttribute('rel') === 'stylesheet') {
Expand All @@ -149,6 +217,41 @@ 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(/&lt;(\/?script)(.*?)&gt;/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: () => {} }
}
})
}

static template () {
return `
<textarea style="display:inherit; width:inherit; height:inherit;"></textarea>
Expand Down