add selection.js which used for control cursor and selection

This commit is contained in:
Jocs 2017-11-14 17:39:51 +08:00
parent 661225e0a7
commit 3a58e6a5be
49 changed files with 944 additions and 193 deletions

21
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -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:

17
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -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!

View File

@ -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"

57
src/editor/config.js Normal file
View File

@ -0,0 +1,57 @@
import {
getUniqueId
} from './utils.js'
const getNewParagraph = set => {
return {
parentType: null,
id: getUniqueId(set),
active: true,
markedText: '<br>',
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
}

52
src/editor/index.js Normal file
View File

@ -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

686
src/editor/selection.js Normal file
View File

@ -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()

View File

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

View File

@ -4,7 +4,7 @@
</template>
<script>
import Aganippe from '@/editor'
import Aganippe from '../../editor'
export default {
data () {
return {
@ -24,6 +24,6 @@
</script>
<style scoped>
@import '../themes/github.css';
@import '../editor/index.css';
@import '../../editor/themes/github.css';
@import '../../editor/index.css';
</style>

View File

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

View File

@ -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

View File

@ -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

View File

@ -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] 说明选择范围,如果相等,说明是光标位置。
}
]
}
]