diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 00000000..4f70cb6f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,21 @@
+### Description
+
+[Description of the bug or feature]
+
+### Steps to reproduce
+
+1. [First step]
+2. [Second step]
+3. [and so on...]
+
+**Expected behavior:** [What you expected to happen]
+
+**Actual behavior:** [What actually happened]
+
+**Link to an example:** [If you're reporting a bug that's not reproducible on our [demo page](https://yabwe.github.io/medium-editor/demo.html), please try to reproduce it on [JSFiddle](https://jsfiddle.net/), [JS Bin](https://jsbin.com), [CodePen](http://codepen.io/) or a similar service and paste a link here]
+
+### Versions
+
+- medium-editor:
+- browser:
+- OS:
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000..5441b1af
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,17 @@
+| Q | A
+| ---------------- | ---
+| Bug fix? | yes/no
+| New feature? | yes/no
+| BC breaks? | yes/no
+| Deprecations? | yes/no
+| New tests added? | yes/not needed
+| Fixed tickets | comma-separated list of tickets fixed by the PR, if any
+| License | MIT
+
+### Description
+
+[Description of the bug or feature]
+
+--
+
+#### Please, don't submit `/dist` files with your PR!
diff --git a/package.json b/package.json
index 961ef497..5a097b23 100644
--- a/package.json
+++ b/package.json
@@ -57,7 +57,6 @@
},
"dependencies": {
"axios": "^0.16.1",
- "dom-iterator": "^0.3.0",
"vue": "^2.3.3",
"vue-electron": "^1.0.6",
"vuex": "^2.3.1"
diff --git a/src/editor/config.js b/src/editor/config.js
new file mode 100644
index 00000000..2d9ac6fe
--- /dev/null
+++ b/src/editor/config.js
@@ -0,0 +1,57 @@
+import {
+ getUniqueId
+} from './utils.js'
+
+const getNewParagraph = set => {
+ return {
+ parentType: null,
+ id: getUniqueId(set),
+ active: true,
+ markedText: '
',
+ paragraphType: 'p',
+ cursorRange: [0, 0],
+ sections: []
+ }
+}
+
+const paragraphClassName = 'aganippe-paragraph'
+
+const blockContainerElementNames = [
+ // elements our editor generates
+ 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'ul', 'li', 'ol',
+ // all other known block elements
+ 'address', 'article', 'aside', 'audio', 'canvas', 'dd', 'dl', 'dt', 'fieldset',
+ 'figcaption', 'figure', 'footer', 'form', 'header', 'hgroup', 'main', 'nav',
+ 'noscript', 'output', 'section', 'video',
+ 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td'
+]
+
+const emptyElementNames = ['br', 'col', 'colgroup', 'hr', 'img', 'input', 'source', 'wbr']
+// const vm = [
+// {
+// parentType: null,
+// id: 'ag-xxx',
+// active: true, // boolean, 一个 viewModel 只可能有一个 blockType 为 true
+// markedText: 'LUORAN**ransixi**',
+// paragraphType: 'ul',
+// // h1~6/ p / ul / ol / tl: task list / blockCode / blockquote / hr /(p, blockCode, hr 不支持嵌套)
+// cursorRange: [1, 2],
+// sections: [
+// {
+// id: 'ag-xer',
+// active: true,
+// parentType: 'ul',
+// paragraphType: 'p',
+// markedText: 'luoran**luoran**', // string not markdown
+// // active: true 才有光标
+// cursorRange: [1, 2] // 如果 cursorIndex[0] !== cursorIndex[1] 说明选择范围,如果相等,说明是光标位置。
+// }
+// ]
+// }
+// ]
+export {
+ getNewParagraph,
+ paragraphClassName,
+ emptyElementNames,
+ blockContainerElementNames
+}
diff --git a/src/renderer/editor/index.css b/src/editor/index.css
similarity index 100%
rename from src/renderer/editor/index.css
rename to src/editor/index.css
diff --git a/src/editor/index.js b/src/editor/index.js
new file mode 100644
index 00000000..ef6043d0
--- /dev/null
+++ b/src/editor/index.js
@@ -0,0 +1,52 @@
+
+import {
+ paragraph2Element // eslint-disable-line no-unused-vars
+} from './utils.js'
+
+import {
+ getNewParagraph, // eslint-disable-line no-unused-vars
+ paragraphClassName // eslint-disable-line no-unused-vars
+} from './config.js'
+
+import selection from './selection' // eslint-disable-line no-unused-vars
+
+class Aganippe {
+ constructor (container, options) {
+ this.doc = document
+ this.container = container
+ this.viewModel = []
+ this.ids = new Set([]) // use to store element'id
+ this.init()
+ }
+ init () {
+ const { container, ids, viewModel } = this // eslint-disable-line no-unused-vars
+ container.setAttribute('contenteditable', true)
+ container.setAttribute('aganippe-editor-element', true)
+ container.id = 'write'
+ this.generateLastEmptyParagraph()
+ this.handleKeyDown()
+ }
+ generateLastEmptyParagraph () {
+ const { ids, viewModel, container, doc } = this
+ const newParagraph = getNewParagraph(ids)
+ viewModel.push(newParagraph)
+ const emptyElement = paragraph2Element(newParagraph)
+ container.appendChild(emptyElement)
+ selection.moveCursor(doc, emptyElement, 0)
+ console.log(viewModel)
+ }
+
+ handleKeyDown () {
+ this.container.addEventListener('keydown', event => {
+ })
+ }
+
+ getMarkdown () {
+
+ }
+ getHtml () {
+
+ }
+}
+
+export default Aganippe
diff --git a/src/editor/selection.js b/src/editor/selection.js
new file mode 100644
index 00000000..a0d46a09
--- /dev/null
+++ b/src/editor/selection.js
@@ -0,0 +1,686 @@
+/**
+ * This file is copy from [medium-editor](https://github.com/yabwe/medium-editor)
+ * and customize for specialized use.
+ */
+import {
+ isBlockContainer,
+ traverseUp,
+ isAganippeEditorElement,
+ getFirstSelectableLeafNode,
+ isElementAtBeginningOfBlock,
+ findPreviousSibling,
+ getClosestBlockContainer
+} from './utils'
+
+const filterOnlyParentElements = function (node) {
+ if (isBlockContainer(node)) {
+ return NodeFilter.FILTER_ACCEPT
+ } else {
+ return NodeFilter.FILTER_SKIP
+ }
+}
+
+class Selection {
+ findMatchingSelectionParent (testElementFunction, contentWindow) {
+ const selection = contentWindow.getSelection()
+ let range
+ let current
+
+ if (selection.rangeCount === 0) {
+ return false
+ }
+
+ range = selection.getRangeAt(0)
+ current = range.commonAncestorContainer
+
+ return traverseUp(current, testElementFunction)
+ }
+
+ getSelectionElement (contentWindow) {
+ return this.findMatchingSelectionParent(el => {
+ return isAganippeEditorElement(el)
+ }, contentWindow)
+ }
+
+ // http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html
+ // Tim Down
+ exportSelection (root, doc) {
+ if (!root) {
+ return null
+ }
+
+ let selectionState = null
+ const selection = doc.getSelection()
+
+ if (selection.rangeCount > 0) {
+ const range = selection.getRangeAt(0)
+ const preSelectionRange = range.cloneRange()
+ let start
+
+ preSelectionRange.selectNodeContents(root)
+ preSelectionRange.setEnd(range.startContainer, range.startOffset)
+ start = preSelectionRange.toString().length
+
+ selectionState = {
+ start: start,
+ end: start + range.toString().length
+ }
+
+ // Check to see if the selection starts with any images
+ // if so we need to make sure the the beginning of the selection is
+ // set correctly when importing selection
+ if (this.doesRangeStartWithImages(range, doc)) {
+ selectionState.startsWithImage = true
+ }
+
+ // Check to see if the selection has any trailing images
+ // if so, this this means we need to look for them when we import selection
+ const trailingImageCount = this.getTrailingImageCount(root, selectionState, range.endContainer, range.endOffset)
+ if (trailingImageCount) {
+ selectionState.trailingImageCount = trailingImageCount
+ }
+
+ // If start = 0 there may still be an empty paragraph before it, but we don't care.
+ if (start !== 0) {
+ const emptyBlocksIndex = this.getIndexRelativeToAdjacentEmptyBlocks(doc, root, range.startContainer, range.startOffset)
+ if (emptyBlocksIndex !== -1) {
+ selectionState.emptyBlocksIndex = emptyBlocksIndex
+ }
+ }
+ }
+
+ return selectionState
+ }
+
+ // http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html
+ // Tim Down
+ //
+ // {object} selectionState - the selection to import
+ // {DOMElement} root - the root element the selection is being restored inside of
+ // {Document} doc - the document to use for managing selection
+ // {boolean} [favorLaterSelectionAnchor] - defaults to false. If true, import the cursor immediately
+ // subsequent to an anchor tag if it would otherwise be placed right at the trailing edge inside the
+ // anchor. This cursor positioning, even though visually equivalent to the user, can affect behavior
+ // in MS IE.
+ importSelection (selectionState, root, doc, favorLaterSelectionAnchor) {
+ if (!selectionState || !root) {
+ return
+ }
+
+ let range = doc.createRange()
+ range.setStart(root, 0)
+ range.collapse(true)
+
+ let node = root
+ const nodeStack = []
+ let charIndex = 0
+ let foundStart = false
+ let foundEnd = false
+ let trailingImageCount = 0
+ let stop = false
+ let nextCharIndex
+ let allowRangeToStartAtEndOfNode = false
+ let lastTextNode = null
+
+ // When importing selection, the start of the selection may lie at the end of an element
+ // or at the beginning of an element. Since visually there is no difference between these 2
+ // we will try to move the selection to the beginning of an element since this is generally
+ // what users will expect and it's a more predictable behavior.
+ //
+ // However, there are some specific cases when we don't want to do this:
+ // 1) We're attempting to move the cursor outside of the end of an anchor [favorLaterSelectionAnchor = true]
+ // 2) The selection starts with an image, which is special since an image doesn't have any 'content'
+ // as far as selection and ranges are concerned
+ // 3) The selection starts after a specified number of empty block elements (selectionState.emptyBlocksIndex)
+ //
+ // For these cases, we want the selection to start at a very specific location, so we should NOT
+ // automatically move the cursor to the beginning of the first actual chunk of text
+ if (favorLaterSelectionAnchor || selectionState.startsWithImage || typeof selectionState.emptyBlocksIndex !== 'undefined') {
+ allowRangeToStartAtEndOfNode = true
+ }
+
+ while (!stop && node) {
+ // Only iterate over elements and text nodes
+ if (node.nodeType > 3) {
+ node = nodeStack.pop()
+ continue
+ }
+
+ // If we hit a text node, we need to add the amount of characters to the overall count
+ if (node.nodeType === 3 && !foundEnd) {
+ nextCharIndex = charIndex + node.length
+ // Check if we're at or beyond the start of the selection we're importing
+ if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) {
+ // NOTE: We only want to allow a selection to start at the END of an element if
+ // allowRangeToStartAtEndOfNode is true
+ if (allowRangeToStartAtEndOfNode || selectionState.start < nextCharIndex) {
+ range.setStart(node, selectionState.start - charIndex)
+ foundStart = true
+ } else {
+ // We're at the end of a text node where the selection could start but we shouldn't
+ // make the selection start here because allowRangeToStartAtEndOfNode is false.
+ // However, we should keep a reference to this node in case there aren't any more
+ // text nodes after this, so that we have somewhere to import the selection to
+ lastTextNode = node
+ }
+ }
+ // We've found the start of the selection, check if we're at or beyond the end of the selection we're importing
+ if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) {
+ if (!selectionState.trailingImageCount) {
+ range.setEnd(node, selectionState.end - charIndex)
+ stop = true
+ } else {
+ foundEnd = true
+ }
+ }
+ charIndex = nextCharIndex
+ } else {
+ if (selectionState.trailingImageCount && foundEnd) {
+ if (node.nodeName.toLowerCase() === 'img') {
+ trailingImageCount++
+ }
+ if (trailingImageCount === selectionState.trailingImageCount) {
+ // Find which index the image is in its parent's children
+ let endIndex = 0
+ while (node.parentNode.childNodes[endIndex] !== node) {
+ endIndex++
+ }
+ range.setEnd(node.parentNode, endIndex + 1)
+ stop = true
+ }
+ }
+
+ if (!stop && node.nodeType === 1) {
+ // this is an element
+ // add all its children to the stack
+ let i = node.childNodes.length - 1
+ while (i >= 0) {
+ nodeStack.push(node.childNodes[i])
+ i -= 1
+ }
+ }
+ }
+
+ if (!stop) {
+ node = nodeStack.pop()
+ }
+ }
+
+ // If we've gone through the entire text but didn't find the beginning of a text node
+ // to make the selection start at, we should fall back to starting the selection
+ // at the END of the last text node we found
+ if (!foundStart && lastTextNode) {
+ range.setStart(lastTextNode, lastTextNode.length)
+ range.setEnd(lastTextNode, lastTextNode.length)
+ }
+
+ if (typeof selectionState.emptyBlocksIndex !== 'undefined') {
+ range = this.importSelectionMoveCursorPastBlocks(doc, root, selectionState.emptyBlocksIndex, range)
+ }
+
+ // If the selection is right at the ending edge of a link, put it outside the anchor tag instead of inside.
+ if (favorLaterSelectionAnchor) {
+ range = this.importSelectionMoveCursorPastAnchor(selectionState, range)
+ }
+
+ this.selectRange(doc, range)
+ }
+
+ // Utility method called from importSelection only
+ importSelectionMoveCursorPastAnchor (selectionState, range) {
+ const nodeInsideAnchorTagFunction = function (node) {
+ return node.nodeName.toLowerCase() === 'a'
+ }
+ if (selectionState.start === selectionState.end &&
+ range.startContainer.nodeType === 3 &&
+ range.startOffset === range.startContainer.nodeValue.length &&
+ traverseUp(range.startContainer, nodeInsideAnchorTagFunction)) {
+ let prevNode = range.startContainer
+ let currentNode = range.startContainer.parentNode
+ while (currentNode !== null && currentNode.nodeName.toLowerCase() !== 'a') {
+ if (currentNode.childNodes[currentNode.childNodes.length - 1] !== prevNode) {
+ currentNode = null
+ } else {
+ prevNode = currentNode
+ currentNode = currentNode.parentNode
+ }
+ }
+ if (currentNode !== null && currentNode.nodeName.toLowerCase() === 'a') {
+ let currentNodeIndex = null
+ for (let i = 0; currentNodeIndex === null && i < currentNode.parentNode.childNodes.length; i++) {
+ if (currentNode.parentNode.childNodes[i] === currentNode) {
+ currentNodeIndex = i
+ }
+ }
+ range.setStart(currentNode.parentNode, currentNodeIndex + 1)
+ range.collapse(true)
+ }
+ }
+ return range
+ }
+
+ // Uses the emptyBlocksIndex calculated by getIndexRelativeToAdjacentEmptyBlocks
+ // to move the cursor back to the start of the correct paragraph
+ importSelectionMoveCursorPastBlocks (doc, root, index = 1, range) {
+ const treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false)
+ let startContainer = range.startContainer
+ let startBlock
+ let targetNode
+ let currIndex = 0
+ // If index is 0, we still want to move to the next block
+
+ // Chrome counts newlines and spaces that separate block elements as actual elements.
+ // If the selection is inside one of these text nodes, and it has a previous sibling
+ // which is a block element, we want the treewalker to start at the previous sibling
+ // and NOT at the parent of the textnode
+ if (startContainer.nodeType === 3 && isBlockContainer(startContainer.previousSibling)) {
+ startBlock = startContainer.previousSibling
+ } else {
+ startBlock = getClosestBlockContainer(startContainer)
+ }
+
+ // Skip over empty blocks until we hit the block we want the selection to be in
+ while (treeWalker.nextNode()) {
+ if (!targetNode) {
+ // Loop through all blocks until we hit the starting block element
+ if (startBlock === treeWalker.currentNode) {
+ targetNode = treeWalker.currentNode
+ }
+ } else {
+ targetNode = treeWalker.currentNode
+ currIndex++
+ // We hit the target index, bail
+ if (currIndex === index) {
+ break
+ }
+ // If we find a non-empty block, ignore the emptyBlocksIndex and just put selection here
+ if (targetNode.textContent.length > 0) {
+ break
+ }
+ }
+ }
+
+ if (!targetNode) {
+ targetNode = startBlock
+ }
+
+ // We're selecting a high-level block node, so make sure the cursor gets moved into the deepest
+ // element at the beginning of the block
+ range.setStart(getFirstSelectableLeafNode(targetNode), 0)
+
+ return range
+ }
+
+ // Returns -1 unless the cursor is at the beginning of a paragraph/block
+ // If the paragraph/block is preceeded by empty paragraphs/block (with no text)
+ // it will return the number of empty paragraphs before the cursor.
+ // Otherwise, it will return 0, which indicates the cursor is at the beginning
+ // of a paragraph/block, and not at the end of the paragraph/block before it
+ getIndexRelativeToAdjacentEmptyBlocks (doc, root, cursorContainer, cursorOffset) {
+ // If there is text in front of the cursor, that means there isn't only empty blocks before it
+ if (cursorContainer.textContent.length > 0 && cursorOffset > 0) {
+ return -1
+ }
+
+ // Check if the block that contains the cursor has any other text in front of the cursor
+ let node = cursorContainer
+ if (node.nodeType !== 3) {
+ node = cursorContainer.childNodes[cursorOffset]
+ }
+ if (node) {
+ // The element isn't at the beginning of a block, so it has content before it
+ if (!isElementAtBeginningOfBlock(node)) {
+ return -1
+ }
+
+ const previousSibling = findPreviousSibling(node)
+ // If there is no previous sibling, this is the first text element in the editor
+ if (!previousSibling) {
+ return -1
+ } else if (previousSibling.nodeValue) {
+ // If the previous sibling has text, then there are no empty blocks before this
+ return -1
+ }
+ }
+
+ // Walk over block elements, counting number of empty blocks between last piece of text
+ // and the block the cursor is in
+ const closestBlock = getClosestBlockContainer(cursorContainer)
+ const treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false)
+ let emptyBlocksCount = 0
+ while (treeWalker.nextNode()) {
+ const blockIsEmpty = treeWalker.currentNode.textContent === ''
+ if (blockIsEmpty || emptyBlocksCount > 0) {
+ emptyBlocksCount += 1
+ }
+ if (treeWalker.currentNode === closestBlock) {
+ return emptyBlocksCount
+ }
+ if (!blockIsEmpty) {
+ emptyBlocksCount = 0
+ }
+ }
+
+ return emptyBlocksCount
+ }
+
+ // Returns true if the selection range begins with an image tag
+ // Returns false if the range starts with any non empty text nodes
+ doesRangeStartWithImages (range, doc) {
+ if (range.startOffset !== 0 || range.startContainer.nodeType !== 1) {
+ return false
+ }
+
+ if (range.startContainer.nodeName.toLowerCase() === 'img') {
+ return true
+ }
+
+ const img = range.startContainer.querySelector('img')
+ if (!img) {
+ return false
+ }
+
+ const treeWalker = doc.createTreeWalker(range.startContainer, NodeFilter.SHOW_ALL, null, false)
+ while (treeWalker.nextNode()) {
+ const next = treeWalker.currentNode
+ // If we hit the image, then there isn't any text before the image so
+ // the image is at the beginning of the range
+ if (next === img) {
+ break
+ }
+ // If we haven't hit the iamge, but found text that contains content
+ // then the range doesn't start with an image
+ if (next.nodeValue) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ getTrailingImageCount (root, selectionState, endContainer, endOffset) {
+ // If the endOffset of a range is 0, the endContainer doesn't contain images
+ // If the endContainer is a text node, there are no trailing images
+ if (endOffset === 0 || endContainer.nodeType !== 1) {
+ return 0
+ }
+
+ // If the endContainer isn't an image, and doesn't have an image descendants
+ // there are no trailing images
+ if (endContainer.nodeName.toLowerCase() !== 'img' && !endContainer.querySelector('img')) {
+ return 0
+ }
+
+ let lastNode = endContainer.childNodes[endOffset - 1]
+ while (lastNode.hasChildNodes()) {
+ lastNode = lastNode.lastChild
+ }
+
+ let node = root
+ const nodeStack = []
+ let charIndex = 0
+ let foundStart = false
+ let foundEnd = false
+ let stop = false
+ let nextCharIndex
+ let trailingImages = 0
+
+ while (!stop && node) {
+ // Only iterate over elements and text nodes
+ if (node.nodeType > 3) {
+ node = nodeStack.pop()
+ continue
+ }
+
+ if (node.nodeType === 3 && !foundEnd) {
+ trailingImages = 0
+ nextCharIndex = charIndex + node.length
+ if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) {
+ foundStart = true
+ }
+ if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) {
+ foundEnd = true
+ }
+ charIndex = nextCharIndex
+ } else {
+ if (node.nodeName.toLowerCase() === 'img') {
+ trailingImages++
+ }
+
+ if (node === lastNode) {
+ stop = true
+ } else if (node.nodeType === 1) {
+ // this is an element
+ // add all its children to the stack
+ let i = node.childNodes.length - 1
+ while (i >= 0) {
+ nodeStack.push(node.childNodes[i])
+ i -= 1
+ }
+ }
+ }
+
+ if (!stop) {
+ node = nodeStack.pop()
+ }
+ }
+
+ return trailingImages
+ }
+
+ // determine if the current selection contains any 'content'
+ // content being any non-white space text or an image
+ selectionContainsContent (doc) {
+ const sel = doc.getSelection()
+
+ // collapsed selection or selection withour range doesn't contain content
+ if (!sel || sel.isCollapsed || !sel.rangeCount) {
+ return false
+ }
+
+ // if toString() contains any text, the selection contains some content
+ if (sel.toString().trim() !== '') {
+ return true
+ }
+
+ // if selection contains only image(s), it will return empty for toString()
+ // so check for an image manually
+ const selectionNode = this.getSelectedParentElement(sel.getRangeAt(0))
+ if (selectionNode) {
+ if (selectionNode.nodeName.toLowerCase() === 'img' ||
+ (selectionNode.nodeType === 1 && selectionNode.querySelector('img'))) {
+ return true
+ }
+ }
+
+ return false
+ }
+
+ selectionInContentEditableFalse (contentWindow) {
+ // determine if the current selection is exclusively inside
+ // a contenteditable="false", though treat the case of an
+ // explicit contenteditable="true" inside a "false" as false.
+ let sawtrue
+ const sawfalse = this.findMatchingSelectionParent(function (el) {
+ const ce = el && el.getAttribute('contenteditable')
+ if (ce === 'true') {
+ sawtrue = true
+ }
+ return el.nodeName !== '#text' && ce === 'false'
+ }, contentWindow)
+
+ return !sawtrue && sawfalse
+ }
+
+ // http://stackoverflow.com/questions/4176923/html-of-selected-text
+ // by Tim Down
+ getSelectionHtml (doc) {
+ let i
+ let html = ''
+ const sel = doc.getSelection()
+ let len
+ let container
+ if (sel.rangeCount) {
+ container = doc.createElement('div')
+ for (i = 0, len = sel.rangeCount; i < len; i += 1) {
+ container.appendChild(sel.getRangeAt(i).cloneContents())
+ }
+ html = container.innerHTML
+ }
+ return html
+ }
+
+ /**
+ * Find the caret position within an element irrespective of any inline tags it may contain.
+ *
+ * @param {DOMElement} An element containing the cursor to find offsets relative to.
+ * @param {Range} A Range representing cursor position. Will window.getSelection if none is passed.
+ * @return {Object} 'left' and 'right' attributes contain offsets from begining and end of Element
+ */
+ getCaretOffsets (element, range) {
+ let preCaretRange
+ let postCaretRange
+
+ if (!range) {
+ range = window.getSelection().getRangeAt(0)
+ }
+
+ preCaretRange = range.cloneRange()
+ postCaretRange = range.cloneRange()
+
+ preCaretRange.selectNodeContents(element)
+ preCaretRange.setEnd(range.endContainer, range.endOffset)
+
+ postCaretRange.selectNodeContents(element)
+ postCaretRange.setStart(range.endContainer, range.endOffset)
+
+ return {
+ left: preCaretRange.toString().length,
+ right: postCaretRange.toString().length
+ }
+ }
+
+ // http://stackoverflow.com/questions/15867542/range-object-get-selection-parent-node-chrome-vs-firefox
+ rangeSelectsSingleNode (range) {
+ const startNode = range.startContainer
+ return startNode === range.endContainer &&
+ startNode.hasChildNodes() &&
+ range.endOffset === range.startOffset + 1
+ }
+
+ getSelectedParentElement (range) {
+ if (!range) {
+ return null
+ }
+
+ // Selection encompasses a single element
+ if (this.rangeSelectsSingleNode(range) && range.startContainer.childNodes[range.startOffset].nodeType !== 3) {
+ return range.startContainer.childNodes[range.startOffset]
+ }
+
+ // Selection range starts inside a text node, so get its parent
+ if (range.startContainer.nodeType === 3) {
+ return range.startContainer.parentNode
+ }
+
+ // Selection starts inside an element
+ return range.startContainer
+ }
+
+ getSelectedElements (doc) {
+ const selection = doc.getSelection()
+ let range
+ let toRet
+ let currNode
+
+ if (!selection.rangeCount || selection.isCollapsed || !selection.getRangeAt(0).commonAncestorContainer) {
+ return []
+ }
+
+ range = selection.getRangeAt(0)
+
+ if (range.commonAncestorContainer.nodeType === 3) {
+ toRet = []
+ currNode = range.commonAncestorContainer
+ while (currNode.parentNode && currNode.parentNode.childNodes.length === 1) {
+ toRet.push(currNode.parentNode)
+ currNode = currNode.parentNode
+ }
+
+ return toRet
+ }
+
+ return [].filter.call(range.commonAncestorContainer.getElementsByTagName('*'), function (el) {
+ return (typeof selection.containsNode === 'function') ? selection.containsNode(el, true) : true
+ })
+ }
+
+ selectNode (node, doc) {
+ const range = doc.createRange()
+ range.selectNodeContents(node)
+ this.selectRange(doc, range)
+ }
+
+ select (doc, startNode, startOffset, endNode, endOffset) {
+ const range = doc.createRange()
+ range.setStart(startNode, startOffset)
+ if (endNode) {
+ range.setEnd(endNode, endOffset)
+ } else {
+ range.collapse(true)
+ }
+ this.selectRange(doc, range)
+ return range
+ }
+
+ /**
+ * Clear the current highlighted selection and set the caret to the start or the end of that prior selection, defaults to end.
+ *
+ * @param {DomDocument} doc Current document
+ * @param {boolean} moveCursorToStart A boolean representing whether or not to set the caret to the beginning of the prior selection.
+ */
+ clearSelection (doc, moveCursorToStart) {
+ if (moveCursorToStart) {
+ doc.getSelection().collapseToStart()
+ } else {
+ doc.getSelection().collapseToEnd()
+ }
+ }
+
+ /**
+ * Move cursor to the given node with the given offset.
+ *
+ * @param {DomDocument} doc Current document
+ * @param {DomElement} node Element where to jump
+ * @param {integer} offset Where in the element should we jump, 0 by default
+ */
+ moveCursor (doc, node, offset) {
+ this.select(doc, node, offset)
+ }
+
+ getSelectionRange (ownerDocument) {
+ const selection = ownerDocument.getSelection()
+ if (selection.rangeCount === 0) {
+ return null
+ }
+ return selection.getRangeAt(0)
+ }
+
+ selectRange (ownerDocument, range) {
+ const selection = ownerDocument.getSelection()
+
+ selection.removeAllRanges()
+ selection.addRange(range)
+ }
+
+ // http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi
+ // by You
+ getSelectionStart (ownerDocument) {
+ const node = ownerDocument.getSelection().anchorNode
+ const startNode = (node && node.nodeType === 3 ? node.parentNode : node)
+
+ return startNode
+ }
+}
+
+export default new Selection()
diff --git a/src/renderer/themes/github.css b/src/editor/themes/github.css
similarity index 100%
rename from src/renderer/themes/github.css
rename to src/editor/themes/github.css
diff --git a/src/renderer/themes/github/300.woff b/src/editor/themes/github/300.woff
similarity index 100%
rename from src/renderer/themes/github/300.woff
rename to src/editor/themes/github/300.woff
diff --git a/src/renderer/themes/github/400.woff b/src/editor/themes/github/400.woff
similarity index 100%
rename from src/renderer/themes/github/400.woff
rename to src/editor/themes/github/400.woff
diff --git a/src/renderer/themes/github/400i.woff b/src/editor/themes/github/400i.woff
similarity index 100%
rename from src/renderer/themes/github/400i.woff
rename to src/editor/themes/github/400i.woff
diff --git a/src/renderer/themes/github/600i.woff b/src/editor/themes/github/600i.woff
similarity index 100%
rename from src/renderer/themes/github/600i.woff
rename to src/editor/themes/github/600i.woff
diff --git a/src/renderer/themes/github/700.woff b/src/editor/themes/github/700.woff
similarity index 100%
rename from src/renderer/themes/github/700.woff
rename to src/editor/themes/github/700.woff
diff --git a/src/renderer/themes/github/700i.woff b/src/editor/themes/github/700i.woff
similarity index 100%
rename from src/renderer/themes/github/700i.woff
rename to src/editor/themes/github/700i.woff
diff --git a/src/renderer/themes/gothic.css b/src/editor/themes/gothic.css
similarity index 100%
rename from src/renderer/themes/gothic.css
rename to src/editor/themes/gothic.css
diff --git a/src/renderer/themes/gothic/GUST e-foundry License.txt b/src/editor/themes/gothic/GUST e-foundry License.txt
similarity index 100%
rename from src/renderer/themes/gothic/GUST e-foundry License.txt
rename to src/editor/themes/gothic/GUST e-foundry License.txt
diff --git a/src/renderer/themes/gothic/texgyreadventor-bold.otf b/src/editor/themes/gothic/texgyreadventor-bold.otf
similarity index 100%
rename from src/renderer/themes/gothic/texgyreadventor-bold.otf
rename to src/editor/themes/gothic/texgyreadventor-bold.otf
diff --git a/src/renderer/themes/gothic/texgyreadventor-bolditalic.otf b/src/editor/themes/gothic/texgyreadventor-bolditalic.otf
similarity index 100%
rename from src/renderer/themes/gothic/texgyreadventor-bolditalic.otf
rename to src/editor/themes/gothic/texgyreadventor-bolditalic.otf
diff --git a/src/renderer/themes/gothic/texgyreadventor-italic.otf b/src/editor/themes/gothic/texgyreadventor-italic.otf
similarity index 100%
rename from src/renderer/themes/gothic/texgyreadventor-italic.otf
rename to src/editor/themes/gothic/texgyreadventor-italic.otf
diff --git a/src/renderer/themes/gothic/texgyreadventor-regular.otf b/src/editor/themes/gothic/texgyreadventor-regular.otf
similarity index 100%
rename from src/renderer/themes/gothic/texgyreadventor-regular.otf
rename to src/editor/themes/gothic/texgyreadventor-regular.otf
diff --git a/src/renderer/themes/newsprint.css b/src/editor/themes/newsprint.css
similarity index 100%
rename from src/renderer/themes/newsprint.css
rename to src/editor/themes/newsprint.css
diff --git a/src/renderer/themes/newsprint/OFL.txt b/src/editor/themes/newsprint/OFL.txt
similarity index 100%
rename from src/renderer/themes/newsprint/OFL.txt
rename to src/editor/themes/newsprint/OFL.txt
diff --git a/src/renderer/themes/newsprint/PT_Serif-Web-Bold.ttf b/src/editor/themes/newsprint/PT_Serif-Web-Bold.ttf
similarity index 100%
rename from src/renderer/themes/newsprint/PT_Serif-Web-Bold.ttf
rename to src/editor/themes/newsprint/PT_Serif-Web-Bold.ttf
diff --git a/src/renderer/themes/newsprint/PT_Serif-Web-BoldItalic.ttf b/src/editor/themes/newsprint/PT_Serif-Web-BoldItalic.ttf
similarity index 100%
rename from src/renderer/themes/newsprint/PT_Serif-Web-BoldItalic.ttf
rename to src/editor/themes/newsprint/PT_Serif-Web-BoldItalic.ttf
diff --git a/src/renderer/themes/newsprint/PT_Serif-Web-Italic.ttf b/src/editor/themes/newsprint/PT_Serif-Web-Italic.ttf
similarity index 100%
rename from src/renderer/themes/newsprint/PT_Serif-Web-Italic.ttf
rename to src/editor/themes/newsprint/PT_Serif-Web-Italic.ttf
diff --git a/src/renderer/themes/newsprint/PT_Serif-Web-Regular.ttf b/src/editor/themes/newsprint/PT_Serif-Web-Regular.ttf
similarity index 100%
rename from src/renderer/themes/newsprint/PT_Serif-Web-Regular.ttf
rename to src/editor/themes/newsprint/PT_Serif-Web-Regular.ttf
diff --git a/src/renderer/themes/night.css b/src/editor/themes/night.css
similarity index 100%
rename from src/renderer/themes/night.css
rename to src/editor/themes/night.css
diff --git a/src/renderer/themes/night/mermaid.dark.css b/src/editor/themes/night/mermaid.dark.css
similarity index 100%
rename from src/renderer/themes/night/mermaid.dark.css
rename to src/editor/themes/night/mermaid.dark.css
diff --git a/src/renderer/themes/pixyll.css b/src/editor/themes/pixyll.css
similarity index 100%
rename from src/renderer/themes/pixyll.css
rename to src/editor/themes/pixyll.css
diff --git a/src/renderer/themes/pixyll/Lato-Black.ttf b/src/editor/themes/pixyll/Lato-Black.ttf
similarity index 100%
rename from src/renderer/themes/pixyll/Lato-Black.ttf
rename to src/editor/themes/pixyll/Lato-Black.ttf
diff --git a/src/renderer/themes/pixyll/Lato-BlackItalic.ttf b/src/editor/themes/pixyll/Lato-BlackItalic.ttf
similarity index 100%
rename from src/renderer/themes/pixyll/Lato-BlackItalic.ttf
rename to src/editor/themes/pixyll/Lato-BlackItalic.ttf
diff --git a/src/renderer/themes/pixyll/Lato-Hairline.ttf b/src/editor/themes/pixyll/Lato-Hairline.ttf
similarity index 100%
rename from src/renderer/themes/pixyll/Lato-Hairline.ttf
rename to src/editor/themes/pixyll/Lato-Hairline.ttf
diff --git a/src/renderer/themes/pixyll/Lato-Light.ttf b/src/editor/themes/pixyll/Lato-Light.ttf
similarity index 100%
rename from src/renderer/themes/pixyll/Lato-Light.ttf
rename to src/editor/themes/pixyll/Lato-Light.ttf
diff --git a/src/renderer/themes/pixyll/Merriweather-Black.ttf b/src/editor/themes/pixyll/Merriweather-Black.ttf
similarity index 100%
rename from src/renderer/themes/pixyll/Merriweather-Black.ttf
rename to src/editor/themes/pixyll/Merriweather-Black.ttf
diff --git a/src/renderer/themes/pixyll/Merriweather-BlackItalic.ttf b/src/editor/themes/pixyll/Merriweather-BlackItalic.ttf
similarity index 100%
rename from src/renderer/themes/pixyll/Merriweather-BlackItalic.ttf
rename to src/editor/themes/pixyll/Merriweather-BlackItalic.ttf
diff --git a/src/renderer/themes/pixyll/Merriweather-Bold.ttf b/src/editor/themes/pixyll/Merriweather-Bold.ttf
similarity index 100%
rename from src/renderer/themes/pixyll/Merriweather-Bold.ttf
rename to src/editor/themes/pixyll/Merriweather-Bold.ttf
diff --git a/src/renderer/themes/pixyll/Merriweather-BoldItalic.ttf b/src/editor/themes/pixyll/Merriweather-BoldItalic.ttf
similarity index 100%
rename from src/renderer/themes/pixyll/Merriweather-BoldItalic.ttf
rename to src/editor/themes/pixyll/Merriweather-BoldItalic.ttf
diff --git a/src/renderer/themes/pixyll/Merriweather-Italic.ttf b/src/editor/themes/pixyll/Merriweather-Italic.ttf
similarity index 100%
rename from src/renderer/themes/pixyll/Merriweather-Italic.ttf
rename to src/editor/themes/pixyll/Merriweather-Italic.ttf
diff --git a/src/renderer/themes/pixyll/Merriweather-Light.ttf b/src/editor/themes/pixyll/Merriweather-Light.ttf
similarity index 100%
rename from src/renderer/themes/pixyll/Merriweather-Light.ttf
rename to src/editor/themes/pixyll/Merriweather-Light.ttf
diff --git a/src/renderer/themes/pixyll/Merriweather-LightItalic.ttf b/src/editor/themes/pixyll/Merriweather-LightItalic.ttf
similarity index 100%
rename from src/renderer/themes/pixyll/Merriweather-LightItalic.ttf
rename to src/editor/themes/pixyll/Merriweather-LightItalic.ttf
diff --git a/src/renderer/themes/pixyll/Merriweather-Regular.ttf b/src/editor/themes/pixyll/Merriweather-Regular.ttf
similarity index 100%
rename from src/renderer/themes/pixyll/Merriweather-Regular.ttf
rename to src/editor/themes/pixyll/Merriweather-Regular.ttf
diff --git a/src/renderer/themes/pixyll/OFL.txt b/src/editor/themes/pixyll/OFL.txt
similarity index 100%
rename from src/renderer/themes/pixyll/OFL.txt
rename to src/editor/themes/pixyll/OFL.txt
diff --git a/src/renderer/themes/whitey.css b/src/editor/themes/whitey.css
similarity index 100%
rename from src/renderer/themes/whitey.css
rename to src/editor/themes/whitey.css
diff --git a/src/renderer/editor/utils.js b/src/editor/utils.js
similarity index 52%
rename from src/renderer/editor/utils.js
rename to src/editor/utils.js
index 81e06b61..02439fe1 100644
--- a/src/renderer/editor/utils.js
+++ b/src/editor/utils.js
@@ -1,5 +1,7 @@
import {
- paragraphClassName
+ emptyElementNames,
+ paragraphClassName,
+ blockContainerElementNames
} from './config.js'
/**
* RegExp constants
@@ -118,32 +120,127 @@ const checkLineBreakUpdate = text => {
* viewModel2Html
*/
-const paragraph2Html = paph => {
+const paragraph2Element = paph => {
const { id, paragraphType, markedText, cursorRange } = paph
- let p = null
+ let element = null
switch (paragraphType) {
case 'p':
- p = document.createElement('p')
- p.id = id
- p.classList.add(paragraphClassName)
- p.innerHTML = markedText2Html(markedText, cursorRange)
+ element = document.createElement('p')
+ element.id = id
+ element.classList.add(paragraphClassName)
+ element.innerHTML = markedText2Html(markedText, cursorRange)
break
default: break
}
- p.id = id
- return p.outerHTML
+ element.id = id
+ return element
}
const viewModel2Html = vm => {
- const htmls = vm.map(p => paragraph2Html(p))
+ const htmls = vm.map(p => paragraph2Element(p).outerHTML)
return htmls.join('\n')
}
+const isBlockContainer = element => {
+ return element && element.nodeType !== 3 &&
+ blockContainerElementNames.indexOf(element.nodeName.toLowerCase()) !== -1
+}
+
+const isAganippeEditorElement = element => {
+ return element && element.getAttribute && !!element.getAttribute('aganippe-editor-element')
+}
+
+const traverseUp = (current, testElementFunction) => {
+ if (!current) {
+ return false
+ }
+
+ do {
+ if (current.nodeType === 1) {
+ if (testElementFunction(current)) {
+ return current
+ }
+ // do not traverse upwards past the nearest containing editor
+ if (isAganippeEditorElement(current)) {
+ return false
+ }
+ }
+
+ current = current.parentNode
+ } while (current)
+
+ return false
+}
+
+const getFirstSelectableLeafNode = element => {
+ while (element && element.firstChild) {
+ element = element.firstChild
+ }
+
+ // We don't want to set the selection to an element that can't have children, this messes up Gecko.
+ element = traverseUp(element, el => {
+ return emptyElementNames.indexOf(el.nodeName.toLowerCase()) === -1
+ })
+ // Selecting at the beginning of a table doesn't work in PhantomJS.
+ if (element.nodeName.toLowerCase() === 'table') {
+ const firstCell = element.querySelector('th, td')
+ if (firstCell) {
+ element = firstCell
+ }
+ }
+ return element
+}
+
+const isElementAtBeginningOfBlock = node => {
+ let textVal
+ let sibling
+ while (!isBlockContainer(node) && !isAganippeEditorElement(node)) {
+ sibling = node.previousSibling
+ while (sibling) {
+ textVal = sibling.nodeType === 3 ? sibling.nodeValue : sibling.textContent
+ if (textVal.length > 0) {
+ return false
+ }
+ sibling = sibling.previousSibling
+ }
+ node = node.parentNode
+ }
+ return true
+}
+
+const findPreviousSibling = node => {
+ if (!node || isAganippeEditorElement(node)) {
+ return false
+ }
+
+ var previousSibling = node.previousSibling
+ while (!previousSibling && !isAganippeEditorElement(node.parentNode)) {
+ node = node.parentNode
+ previousSibling = node.previousSibling
+ }
+
+ return previousSibling
+}
+
+const getClosestBlockContainer = node => {
+ return traverseUp(node, node => {
+ return isBlockContainer(node) || isAganippeEditorElement(node)
+ })
+}
+
export {
getUniqueId,
markedText2Html,
checkInlineUpdate,
checkLineBreakUpdate,
viewModel2Html,
- paragraph2Html
+ paragraph2Element,
+
+ isBlockContainer,
+ traverseUp,
+ isAganippeEditorElement,
+ isElementAtBeginningOfBlock,
+ getFirstSelectableLeafNode,
+ findPreviousSibling,
+ getClosestBlockContainer
}
diff --git a/src/renderer/components/Editor.vue b/src/renderer/components/Editor.vue
index 2f8f2d4a..9228a9dc 100644
--- a/src/renderer/components/Editor.vue
+++ b/src/renderer/components/Editor.vue
@@ -4,7 +4,7 @@
diff --git a/src/renderer/editor/config.js b/src/renderer/editor/config.js
deleted file mode 100644
index 264722d5..00000000
--- a/src/renderer/editor/config.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import {
- getUniqueId
-} from './utils.js'
-
-const getNewParagraph = set => {
- return {
- parentType: null,
- id: getUniqueId(set),
- active: true, // boolean, 一个 viewModel 只可能有一个 blockType 为 true
- markedText: ' ',
- // h1~6/ p / ul / ol / tl: task list / blockCode / blockquote / hr /(p, blockCode, hr 不支持嵌套)
- paragraphType: 'p',
- cursorRange: [0, 0],
- sections: []
- }
-}
-
-const paragraphClassName = 'ag-1'
-
-export {
- getNewParagraph,
- paragraphClassName
-}
diff --git a/src/renderer/editor/cursorCtrl.js b/src/renderer/editor/cursorCtrl.js
deleted file mode 100644
index be85429f..00000000
--- a/src/renderer/editor/cursorCtrl.js
+++ /dev/null
@@ -1,93 +0,0 @@
-/**
- * Module Dependencies
- */
-
-import iterator from 'dom-iterator'
-const selection = window.getSelection()
-
-/**
- * Get or set cursor, selection, relative to
- * an element.
- *
- * @param {Element} el
- * @param {Object} pos selection range
- * @return {Object|Undefined}
- */
-
-function position (el, pos) {
- console.log(el)
- /**
- * Get cursor or selection position
- */
-
- if (arguments.length === 1) {
- if (!selection.rangeCount) return
- const indexes = {}
- const range = selection.getRangeAt(0)
- const clone = range.cloneRange()
- clone.selectNodeContents(el)
- clone.setEnd(range.endContainer, range.endOffset)
- indexes.end = clone.toString().length
- clone.setStart(range.startContainer, range.startOffset)
- indexes.start = indexes.end - clone.toString().length
- indexes.atStart = clone.startOffset === 0
- indexes.commonAncestorContainer = clone.commonAncestorContainer
- indexes.endContainer = clone.endContainer
- indexes.startContainer = clone.startContainer
- return indexes
- }
-
- /**
- * Set cursor or selection position
- */
-
- const setSelection = pos.end && (pos.end !== pos.start)
- let length = 0
- const range = document.createRange()
- const it = iterator(el).select(Node.TEXT_NODE).revisit(false)
- let next = it.next()
- let startindex
- let start = pos.start > el.textContent.length ? el.textContent.length : pos.start
- let end = pos.end > el.textContent.length ? el.textContent.length : pos.end
- let atStart = pos.atStart
-
- while (next) {
- const olen = length
- length += next.textContent.length
- console.log(next, length, start)
- // Set start point of selection
- const atLength = atStart ? length > start : length >= start
- if (!startindex && atLength) {
- startindex = true
- range.setStart(next, start - olen)
- if (!setSelection) {
- range.collapse(true)
- makeSelection(el, range)
- break
- }
- }
-
- // Set end point of selection
- if (setSelection && (length >= end)) {
- range.setEnd(next, end - olen)
- makeSelection(el, range)
- break
- }
- next = it.next()
- }
-}
-
-/**
- * add selection / insert cursor.
- *
- * @param {Element} el
- * @param {Range} range
- */
-
-function makeSelection (el, range) {
- el.focus()
- selection.removeAllRanges()
- selection.addRange(range)
-}
-
-export default position
diff --git a/src/renderer/editor/index.js b/src/renderer/editor/index.js
deleted file mode 100644
index 9892d258..00000000
--- a/src/renderer/editor/index.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import {
- viewModel2Html
-} from './utils.js'
-
-import {
- getNewParagraph,
- paragraphClassName
-} from './config.js'
-
-import cursor from './cursorCtrl.js'
-
-class Aganippe {
- constructor (container, options) {
- this.container = container
- this.viewModel = []
- this.ids = new Set([]) // use to store element'id
- this.init()
- }
- init () {
- const { container, ids, viewModel } = this
- container.setAttribute('contenteditable', true)
- container.id = 'write'
- const newParagraph = getNewParagraph(ids)
- viewModel.push(newParagraph)
- container.innerHTML = viewModel2Html(viewModel)
- const ps = container.querySelectorAll(`.${paragraphClassName}`)
- const lastP = ps[ps.length - 1]
- console.log(lastP)
- const start = lastP.textContent.length
- cursor(lastP, { start })
- }
- getMarkdown () {
-
- }
- getHtml () {
-
- }
-}
-
-export default Aganippe
diff --git a/src/renderer/editor/temp.model.js b/src/renderer/editor/temp.model.js
deleted file mode 100644
index fd520c4d..00000000
--- a/src/renderer/editor/temp.model.js
+++ /dev/null
@@ -1,22 +0,0 @@
-const vm = [
- {
- parentType: null,
- id: 'ag-xxx',
- active: true, // boolean, 一个 viewModel 只可能有一个 blockType 为 true
- markedText: 'LUORAN**ransixi**',
- paragraphType: 'ul',
- // h1~6/ p / ul / ol / tl: task list / blockCode / blockquote / hr /(p, blockCode, hr 不支持嵌套)
- cursorRange: [1, 2],
- sections: [
- {
- id: 'ag-xer',
- active: true,
- parentType: 'ul',
- paragraphType: 'p',
- markedText: 'luoran**luoran**', // string not markdown
- // active: true 才有光标
- cursorRange: [1, 2], // 如果 cursorIndex[0] !== cursorIndex[1] 说明选择范围,如果相等,说明是光标位置。
- }
- ]
- }
-]
\ No newline at end of file