diff --git a/src/extensions/libs/markdownItAnchor.js b/src/extensions/libs/markdownItAnchor.js new file mode 100644 index 00000000..70061878 --- /dev/null +++ b/src/extensions/libs/markdownItAnchor.js @@ -0,0 +1,53 @@ +export default (md) => { + md.core.ruler.before('replacements', 'anchors', (state) => { + const anchorHash = {}; + let headingOpenToken; + let headingContent; + state.tokens.forEach((token) => { + if (token.type === 'heading_open') { + headingContent = ''; + headingOpenToken = token; + } else if (token.type === 'heading_close') { + headingOpenToken.headingContent = headingContent; + + // According to http://pandoc.org/README.html#extension-auto_identifiers + let slug = headingContent + .replace(/\s/g, '-') // Replace all spaces and newlines with hyphens + .replace(/[\0-,/:-@[-^`{-~]/g, '') // Remove all punctuation, except underscores, hyphens, and periods + .toLowerCase(); // Convert all alphabetic characters to lowercase + + // Remove everything up to the first letter + let i; + for (i = 0; i < slug.length; i += 1) { + const charCode = slug.charCodeAt(i); + if ((charCode >= 0x61 && charCode <= 0x7A) || charCode > 0x7E) { + break; + } + } + + // If nothing left after this, use `section` + slug = slug.slice(i) || 'section'; + + let anchor = slug; + let index = 1; + while (Object.prototype.hasOwnProperty.call(anchorHash, anchor)) { + anchor = `${slug}-${index}`; + index += 1; + } + anchorHash[anchor] = true; + headingOpenToken.headingAnchor = anchor; + headingOpenToken.attrs = [ + ['id', anchor], + ]; + headingOpenToken = undefined; + } else if (headingOpenToken) { + headingContent += token.children.reduce((result, child) => { + if (child.type !== 'footnote_ref') { + return result + child.content; + } + return result; + }, ''); + } + }); + }); +}; diff --git a/src/extensions/markdownExtension.js b/src/extensions/markdownExtension.js index 0a89c195..30cd0a7c 100644 --- a/src/extensions/markdownExtension.js +++ b/src/extensions/markdownExtension.js @@ -7,6 +7,7 @@ import markdownitImgsize from 'markdown-it-imsize'; import markdownitSub from 'markdown-it-sub'; import markdownitSup from 'markdown-it-sup'; import markdownitTasklist from './libs/markdownItTasklist'; +import markdownitAnchor from './libs/markdownItAnchor'; import extensionSvc from '../services/extensionSvc'; const coreBaseRules = [ @@ -106,58 +107,7 @@ extensionSvc.onInitConverter(0, (markdown, options) => { if (options.tasklist) { markdown.use(markdownitTasklist); } - - markdown.core.ruler.before('replacements', 'anchors', (state) => { - const anchorHash = {}; - let headingOpenToken; - let headingContent; - state.tokens.forEach((token) => { - if (token.type === 'heading_open') { - headingContent = ''; - headingOpenToken = token; - } else if (token.type === 'heading_close') { - headingOpenToken.headingContent = headingContent; - - // According to http://pandoc.org/README.html#extension-auto_identifiers - let slug = headingContent - .replace(/\s/g, '-') // Replace all spaces and newlines with hyphens - .replace(/[\0-,/:-@[-^`{-~]/g, '') // Remove all punctuation, except underscores, hyphens, and periods - .toLowerCase(); // Convert all alphabetic characters to lowercase - - // Remove everything up to the first letter - let i; - for (i = 0; i < slug.length; i += 1) { - const charCode = slug.charCodeAt(i); - if ((charCode >= 0x61 && charCode <= 0x7A) || charCode > 0x7E) { - break; - } - } - - // If nothing left after this, use `section` - slug = slug.slice(i) || 'section'; - - let anchor = slug; - let index = 1; - while (Object.prototype.hasOwnProperty.call(anchorHash, anchor)) { - anchor = `${slug}-${index}`; - index += 1; - } - anchorHash[anchor] = true; - headingOpenToken.headingAnchor = anchor; - headingOpenToken.attrs = [ - ['id', anchor], - ]; - headingOpenToken = undefined; - } else if (headingOpenToken) { - headingContent += token.children.reduce((result, child) => { - if (child.type !== 'footnote_ref') { - return result + child.content; - } - return result; - }, ''); - } - }); - }); + markdown.use(markdownitAnchor); // Wrap tables into a div for scrolling markdown.renderer.rules.table_open = (tokens, idx, opts) => @@ -188,22 +138,24 @@ extensionSvc.onInitConverter(0, (markdown, options) => { }; }); -extensionSvc.onSectionPreview((elt) => { +extensionSvc.onSectionPreview((elt, options, isEditor) => { // Highlight with Prism elt.querySelectorAll('.prism').cl_each((prismElt) => { - if (!prismElt.highlightedWithPrism) { + if (!prismElt.$highlightedWithPrism) { Prism.highlightElement(prismElt); - prismElt.highlightedWithPrism = true; + prismElt.$highlightedWithPrism = true; } }); - // Transform task list spans into checkboxes + // Transform task spans into checkboxes elt.querySelectorAll('span.task-list-item-checkbox').cl_each((spanElt) => { const checkboxElt = document.createElement('input'); checkboxElt.type = 'checkbox'; checkboxElt.className = 'task-list-item-checkbox'; checkboxElt.checked = spanElt.classList.contains('checked'); - checkboxElt.disabled = 'disabled'; + if (!isEditor) { + checkboxElt.disabled = 'disabled'; + } spanElt.parentNode.replaceChild(checkboxElt, spanElt); }); }); diff --git a/src/services/editorSvc.js b/src/services/editorSvc.js index c49d799f..b4002a6b 100644 --- a/src/services/editorSvc.js +++ b/src/services/editorSvc.js @@ -167,7 +167,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils, } else { this.previewElt.appendChild(sectionPreviewElt); } - extensionSvc.sectionPreview(sectionPreviewElt, this.options); + extensionSvc.sectionPreview(sectionPreviewElt, this.options, true); loadingImages = [ ...loadingImages, ...Array.prototype.slice.call(sectionPreviewElt.getElementsByTagName('img')), diff --git a/src/services/extensionSvc.js b/src/services/extensionSvc.js index 2eb6d35f..b2882597 100644 --- a/src/services/extensionSvc.js +++ b/src/services/extensionSvc.js @@ -29,9 +29,9 @@ export default { }); }, - sectionPreview(elt, options) { + sectionPreview(elt, options, isEditor) { sectionPreviewListeners.forEach((listener) => { - listener(elt, options); + listener(elt, options, isEditor); }); }, }; diff --git a/src/services/optional/index.js b/src/services/optional/index.js index 268d3f71..e6efbb97 100644 --- a/src/services/optional/index.js +++ b/src/services/optional/index.js @@ -1,3 +1,4 @@ import './shortcuts'; import './keystrokes'; import './scrollSync'; +import './taskChange'; diff --git a/src/services/optional/taskChange.js b/src/services/optional/taskChange.js new file mode 100644 index 00000000..178a280a --- /dev/null +++ b/src/services/optional/taskChange.js @@ -0,0 +1,47 @@ +import editorSvc from '../editorSvc'; +import store from '../../store'; + +editorSvc.$on('inited', () => { + const getPreviewOffset = (elt) => { + let offset = 0; + if (!elt || elt === editorSvc.previewElt) { + return offset; + } + let previousSibling = elt.previousSibling; + while (previousSibling) { + offset += previousSibling.textContent.length; + previousSibling = previousSibling.previousSibling; + } + return offset + getPreviewOffset(elt.parentNode); + }; + + editorSvc.previewElt.addEventListener('click', (evt) => { + if (evt.target.classList.contains('task-list-item-checkbox')) { + evt.preventDefault(); + if (store.getters['content/isCurrentEditable']) { + const editorContent = editorSvc.clEditor.getContent(); + // Use setTimeout to ensure evt.target.checked has the old value + setTimeout(() => { + // Make sure content has not changed + if (editorContent === editorSvc.clEditor.getContent()) { + const previewOffset = getPreviewOffset(evt.target); + const endOffset = editorSvc.getEditorOffset(previewOffset + 1); + if (endOffset != null) { + const startOffset = editorContent.lastIndexOf('\n', endOffset) + 1; + const line = editorContent.slice(startOffset, endOffset); + const match = line.match(/^([ \t]*(?:[*+-]|\d+\.)[ \t]+\[)[ xX](\] .*)/); + if (match) { + let newContent = editorContent.slice(0, startOffset); + newContent += match[1]; + newContent += evt.target.checked ? ' ' : 'x'; + newContent += match[2]; + newContent += editorContent.slice(endOffset); + editorSvc.clEditor.setContent(newContent, true); + } + } + } + }, 10); + } + } + }); +});