diff --git a/README.md b/README.md index cb41cf7..9a0c121 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,19 @@ 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 + + + + +``` + ### 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..e68cdf3 100644 --- a/src/wc-codemirror.js +++ b/src/wc-codemirror.js @@ -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 @@ -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) @@ -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(/<(\/?script)(.*?)>/g, '<$1$2>') - } - } - let viewportMargin = CodeMirror.defaults.viewportMargin if (this.hasAttribute('viewport-margin')) { const viewportMarginAttr = this.getAttribute('viewport-margin').toLowerCase() @@ -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 () { @@ -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() } @@ -134,7 +202,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 +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(/<(\/?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: () => {} } + } + }) + } + static template () { return `