Reactive task lists

This commit is contained in:
Benoit Schweblin 2018-04-06 19:41:30 +01:00
parent 12c43b960b
commit 0a361a5ca0
6 changed files with 113 additions and 60 deletions

View File

@ -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;
}, '');
}
});
});
};

View File

@ -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');
if (!isEditor) {
checkboxElt.disabled = 'disabled';
}
spanElt.parentNode.replaceChild(checkboxElt, spanElt);
});
});

View File

@ -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')),

View File

@ -29,9 +29,9 @@ export default {
});
},
sectionPreview(elt, options) {
sectionPreview(elt, options, isEditor) {
sectionPreviewListeners.forEach((listener) => {
listener(elt, options);
listener(elt, options, isEditor);
});
},
};

View File

@ -1,3 +1,4 @@
import './shortcuts';
import './keystrokes';
import './scrollSync';
import './taskChange';

View File

@ -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);
}
}
});
});