diff --git a/src/editor/config.js b/src/editor/config.js index 85114930..5514c1a9 100644 --- a/src/editor/config.js +++ b/src/editor/config.js @@ -89,7 +89,9 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([ 'AG_HIGHLIGHT', 'AG_MATH', 'AG_MATH_RENDER', - 'AG_MATH_ERROR' + 'AG_MATH_ERROR', + 'AG_LOOSE_LIST_ITEM', + 'AG_TIGHT_LIST_ITEM' ]) export const codeMirrorConfig = { diff --git a/src/editor/contentState/enterCtrl.js b/src/editor/contentState/enterCtrl.js index 653c2fd7..9ac7dc08 100644 --- a/src/editor/contentState/enterCtrl.js +++ b/src/editor/contentState/enterCtrl.js @@ -187,6 +187,7 @@ const enterCtrl = ContentState => { newBlock = this.createBlockLi(post) newBlock.listItemType = block.listItemType } + newBlock.isLooseListItem = block.isLooseListItem } else { block.text = pre newBlock = this.createBlock(type, post) @@ -218,6 +219,7 @@ const enterCtrl = ContentState => { newBlock = this.createBlockLi() newBlock.listItemType = parent.listItemType } + newBlock.isLooseListItem = parent.isLooseListItem this.insertAfter(newBlock, parent) const index = this.findIndex(parent.children, block) const partChildren = parent.children.splice(index + 1) @@ -245,6 +247,7 @@ const enterCtrl = ContentState => { newBlock = this.createBlockLi() newBlock.listItemType = block.listItemType } + newBlock.isLooseListItem = block.isLooseListItem } else { newBlock = this.createBlock('p') } diff --git a/src/editor/contentState/updateCtrl.js b/src/editor/contentState/updateCtrl.js index 029ed2d9..5e5430a3 100644 --- a/src/editor/contentState/updateCtrl.js +++ b/src/editor/contentState/updateCtrl.js @@ -30,7 +30,7 @@ const updateCtrl = ContentState => { return false } - ContentState.prototype.checkInlineUpdate = function (block) { + ContentState.prototype.checkInlineUpdate = function (block, preferLooseListItem) { if (/th|td|figure/.test(block.type)) return false const { text } = block const parent = this.getParent(block) @@ -43,11 +43,11 @@ const updateCtrl = ContentState => { return true case !!bullet: - this.updateList(block, 'bullet', bullet) + this.updateList(block, 'bullet', preferLooseListItem, bullet) return true case !!tasklist && parent && parent.listItemType === 'bullet': // only `bullet` list item can be update to `task` list item - this.updateTaskListItem(block, 'tasklist', tasklist) + this.updateTaskListItem(block, 'tasklist', preferLooseListItem, tasklist) return true case !!order: @@ -144,7 +144,7 @@ const updateCtrl = ContentState => { this.render() } - ContentState.prototype.updateList = function (block, type, marker = '') { + ContentState.prototype.updateList = function (block, type, preferLooseListItem, marker = '') { const parent = this.getParent(block) const preSibling = this.getPreSibling(block) const nextSibling = this.getNextSibling(block) @@ -155,6 +155,7 @@ const updateCtrl = ContentState => { const endOffset = end.offset const newBlock = this.createBlockLi(newText, block.type) newBlock.listItemType = type + newBlock.isLooseListItem = preferLooseListItem if (preSibling && preSibling.listType === type && nextSibling && nextSibling.listType === type) { this.appendChild(preSibling, newBlock) @@ -224,7 +225,7 @@ const updateCtrl = ContentState => { block.type = 'hr' } - ContentState.prototype.updateState = function (event) { + ContentState.prototype.updateState = function (event, preferLooseListItem) { const { floatBox } = this const { start, end } = selection.getCursorRange() const { start: oldStart, end: oldEnd } = this.cursor @@ -233,7 +234,7 @@ const updateCtrl = ContentState => { } if (event.type === 'click' && start.key !== end.key) { setTimeout(() => { - this.updateState(event) + this.updateState(event, preferLooseListItem) }) } if (event.type === 'input' && oldStart.key !== oldEnd.key) { @@ -326,7 +327,7 @@ const updateCtrl = ContentState => { this.cursor = { start, end } const checkMarkedUpdate = this.checkNeedRender(block) - const checkInlineUpdate = this.checkInlineUpdate(block) + const checkInlineUpdate = this.checkInlineUpdate(block, preferLooseListItem) if (checkMarkedUpdate || checkInlineUpdate || needRender) { this.render() diff --git a/src/editor/index.js b/src/editor/index.js index 96dfa694..3157f98e 100644 --- a/src/editor/index.js +++ b/src/editor/index.js @@ -19,7 +19,7 @@ import './assets/symbolIcon/index.css' class Aganippe { constructor (container, options) { - const { focusMode = false, theme = 'light', markdown = '' } = options + const { focusMode = false, theme = 'light', markdown = '', preferLooseListItem = true } = options this.container = container const eventCenter = this.eventCenter = new EventCenter() const floatBox = this.floatBox = new FloatBox(eventCenter) @@ -31,6 +31,7 @@ class Aganippe { this.markdown = markdown this.fontSize = 16 this.lineHeight = 1.6 + this.preferLooseListItem = preferLooseListItem // private property this._isEditChinese = false this.init() @@ -342,7 +343,7 @@ class Aganippe { // const style = getComputedStyle(target) // if (event.type === 'click' && !style.contenteditable) return if (!this._isEditChinese) { - this.contentState.updateState(event) + this.contentState.updateState(event, this.preferLooseListItem) } if (event.type === 'click' || event.type === 'keyup') { const selectionChanges = this.getSelection() diff --git a/src/editor/parser/StateRender.js b/src/editor/parser/StateRender.js index 699b484d..b806fc16 100644 --- a/src/editor/parser/StateRender.js +++ b/src/editor/parser/StateRender.js @@ -113,6 +113,7 @@ class StateRender { default: break } + blockSelector += block.isLooseListItem ? `.${CLASS_OR_ID['AG_LOOSE_LIST_ITEM']}` : `.${CLASS_OR_ID['AG_TIGHT_LIST_ITEM']}` } if (block.type === 'ol') { Object.assign(data.attrs, { start: block.start }) diff --git a/src/editor/parser/marked.js b/src/editor/parser/marked.js index af8cb205..c0da7a90 100644 --- a/src/editor/parser/marked.js +++ b/src/editor/parser/marked.js @@ -1,3 +1,5 @@ +import { CLASS_OR_ID } from '../config' + /** * marked - a markdown parser * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) @@ -155,7 +157,7 @@ Lexer.prototype.lex = function(src) { Lexer.prototype.token = function(src, top, bq) { var src = src.replace(/^ +$/gm, ''), - next, loose, cap, bull, b, item, space, i, l, checked; + loose, cap, bull, b, item, space, i, l, checked; while (src) { // newline @@ -288,14 +290,18 @@ Lexer.prototype.token = function(src, top, bq) { listType: bull.length > 1 ? (/\d/.test(bull) ? 'order' : 'task') : 'bullet' }); + let next = false; + let prevNext = true; + let listItemIndices = []; + // Get each top-level item. cap = cap[0].match(this.rules.item); - next = false; l = cap.length; i = 0; for (; i < l; i++) { - item = cap[i]; + const itemWithBullet = cap[i]; + item = itemWithBullet; // Remove the list item's bullet // so it is seen as the next token. @@ -331,13 +337,33 @@ Lexer.prototype.token = function(src, top, bq) { } } - // Determine whether item is loose or not. - // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ - // for discount behavior. - loose = next || /\n\n(?!\s*$)/.test(item); - if (i !== l - 1) { - next = item.charAt(item.length - 1) === '\n'; - if (!loose) loose = next; + var prevItem = ''; + if (i === 0) { + prevItem = item; + } else { + prevItem = cap[i - 1]; + } + + // Determine whether item is loose or not. If previous item is loose + // this item is also loose. + loose = next = next || /^ *([*+-]|\d+\.) +\S+\n\n(?!\s*$)/.test(itemWithBullet); + + // Check if previous line ends with a new line. + if (!loose && (i !== 0 || l > 1) && prevItem.length !== 0 && prevItem.charAt(prevItem.length - 1) === '\n') { + loose = next = true; + } + + // A list is either loose or tight, so update previous list items. + if (next && prevNext !== next) { + for(const index of listItemIndices) { + this.tokens[index].type = 'loose_item_start' + } + listItemIndices = []; + } + prevNext = next; + + if (!loose) { + listItemIndices.push(this.tokens.length); } this.tokens.push({ @@ -828,19 +854,26 @@ Renderer.prototype.list = function(body, ordered, taskList) { return '<' + type + classes + '>\n' + body + '' + type + '>\n'; }; -Renderer.prototype.listitem = function(text, checked, listItemType) { - var classes +Renderer.prototype.listitem = function(text, checked, listItemType, loose) { + var classes; switch (listItemType) { case 'order': - classes = ' class="order-list-item"' - break + classes = ' class="order-list-item'; + break; case 'task': - classes = ' class="task-list-item"' - break + classes = ' class="task-list-item'; + break; case 'bullet': - classes = ' class="bullet-list-item"' - break + classes = ' class="bullet-list-item'; + break; + default: + throw new + Error('Invalid state'); } + + // "tight-list-item" is only used to remove
padding + classes += loose ? ` .${CLASS_OR_ID['AG_LOOSE_LIST_ITEM']}"` : ` .${CLASS_OR_ID['AG_TIGHT_LIST_ITEM']}"`; + if (checked === undefined) { return '