From cae2962a199d8e75fad2d151d251c2b70151f4b5 Mon Sep 17 00:00:00 2001 From: Aleksey Fomkin Date: Fri, 27 Aug 2021 11:33:31 +0300 Subject: [PATCH 1/3] close #31: add dynamic wc-content --- src/wc-codemirror.js | 72 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/src/wc-codemirror.js b/src/wc-codemirror.js index 6f860be..396d684 100644 --- a/src/wc-codemirror.js +++ b/src/wc-codemirror.js @@ -46,11 +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() + + const observerConfig = { + childList: true, + characterData: true, + subtree: true + } + + const linkChanged = (e) => { + return e.type === "childList" && + (Array.from(e.addedNodes).some((e) => e.tagName === "LINK") || + Array.from(e.removedNodes).some((e) => e.tagName === "LINK")) + } + + this.__observer = new MutationObserver((mutationsList, observer) => { + if (mutationsList.some(linkChanged)) { + this.refreshStyles(); + } + this.lookupInnerScript((data) => { + this.value = data + }) + }) + this.__observer.observe(this, observerConfig) + this.__initialized = false this.__element = null this.editor = null @@ -60,7 +88,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) @@ -78,14 +106,9 @@ export class WCCodeMirror extends HTMLElement { 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>') - } - } + this.lookupInnerScript((data) => { + this.value = data + }) let viewportMargin = CodeMirror.defaults.viewportMargin if (this.hasAttribute('viewport-margin')) { @@ -102,20 +125,23 @@ export class WCCodeMirror extends HTMLElement { }) 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) + } } disconnectedCallback () { this.editor && this.editor.toTextArea() this.editor = null this.__initialized = false + this.__observer.disconnect() } async setSrc () { @@ -124,7 +150,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() } @@ -155,6 +184,17 @@ export class WCCodeMirror extends HTMLElement { ` } + lookupInnerScript (callback) { + const innerScriptTag = this.querySelector('script') + if (innerScriptTag) { + if (innerScriptTag.getAttribute('type') === 'wc-content') { + let data = WCCodeMirror.dedentText(innerScriptTag.innerHTML) + data = data.replace(/<(\/?script)(.*?)>/g, '<$1$2>') + callback(data) + } + } + } + /** * De-dents the code by getting the padding from the first line, * then removes the same indent amount padding from the rest of the lines From 7a04c2be34d27ad40574d2c2d3ae4f1f21eea787 Mon Sep 17 00:00:00 2001 From: Aleksey Fomkin Date: Sat, 28 Aug 2021 10:43:03 +0300 Subject: [PATCH 2/3] close #35: add the way to contol text marks from DOM --- README.md | 13 +++++ src/wc-codemirror.js | 115 +++++++++++++++++++++++++++++++++---------- 2 files changed, 102 insertions(+), 26 deletions(-) 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 396d684..e68cdf3 100644 --- a/src/wc-codemirror.js +++ b/src/wc-codemirror.js @@ -63,22 +63,61 @@ export class WCCodeMirror extends HTMLElement { subtree: true } - const linkChanged = (e) => { - return e.type === "childList" && - (Array.from(e.addedNodes).some((e) => e.tagName === "LINK") || - Array.from(e.removedNodes).some((e) => e.tagName === "LINK")) + 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) => { - if (mutationsList.some(linkChanged)) { - this.refreshStyles(); - } - this.lookupInnerScript((data) => { - this.value = data + 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 @@ -104,12 +143,6 @@ export class WCCodeMirror extends HTMLElement { if (readOnly === '') readOnly = true else if (readOnly !== 'nocursor') readOnly = false - this.refreshStyles() - - this.lookupInnerScript((data) => { - this.value = data - }) - let viewportMargin = CodeMirror.defaults.viewportMargin if (this.hasAttribute('viewport-margin')) { const viewportMarginAttr = this.getAttribute('viewport-margin').toLowerCase() @@ -124,6 +157,9 @@ export class WCCodeMirror extends HTMLElement { viewportMargin }) + this.refreshStyleLinks() + this.refrestWcContent() + if (this.hasAttribute('src')) { this.setSrc() } @@ -135,6 +171,9 @@ export class WCCodeMirror extends HTMLElement { if (this.__preInitValue !== undefined) { this.setValueForced(this.__preInitValue) } + + // This should be invoked after text set + this.refreshMarkText() } disconnectedCallback () { @@ -163,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') { @@ -178,23 +217,47 @@ export class WCCodeMirror extends HTMLElement { }) } - static template () { - return ` - - ` - } - - lookupInnerScript (callback) { + refrestWcContent () { const innerScriptTag = this.querySelector('script') if (innerScriptTag) { if (innerScriptTag.getAttribute('type') === 'wc-content') { - let data = WCCodeMirror.dedentText(innerScriptTag.innerHTML) - data = data.replace(/<(\/?script)(.*?)>/g, '<$1$2>') - callback(data) + 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 ` + + ` + } + /** * De-dents the code by getting the padding from the first line, * then removes the same indent amount padding from the rest of the lines From a4fe4133acc088c60db0f70f1fbd8d19bda39cc5 Mon Sep 17 00:00:00 2001 From: Aleksey Fomkin Date: Sat, 28 Aug 2021 14:21:09 +0300 Subject: [PATCH 3/3] close #37: add the way to contol gutters from DOM --- README.md | 15 +++- src/wc-codemirror.js | 192 ++++++++++++++++++++++++++++--------------- 2 files changed, 139 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 9a0c121..e8daa5a 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,20 @@ Can be used to mark a range of text with a specific CSS class name. - + + +``` + +### Gutter markers + +Can be used to add extra gutters (beyond of the line number gutter). Lines starts from zero. + +```html + + + + + ``` diff --git a/src/wc-codemirror.js b/src/wc-codemirror.js index e68cdf3..142ba0d 100644 --- a/src/wc-codemirror.js +++ b/src/wc-codemirror.js @@ -57,84 +57,28 @@ export class WCCodeMirror extends HTMLElement { 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.setupMutationObserver() + this.attachShadow({ mode: 'open' }) - 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) + // 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' @@ -174,6 +118,7 @@ export class WCCodeMirror extends HTMLElement { // This should be invoked after text set this.refreshMarkText() + this.refreshGutters() } disconnectedCallback () { @@ -252,6 +197,119 @@ export class WCCodeMirror extends HTMLElement { }) } + 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.cloneNode(true) } + }) + } + }) + 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) + }) + }) + } + + 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) + } + static template () { return `