mirror of
https://github.com/marktext/marktext.git
synced 2025-05-03 19:42:07 +08:00
add selection.js which used for control cursor and selection
This commit is contained in:
parent
661225e0a7
commit
3a58e6a5be
21
.github/ISSUE_TEMPLATE.md
vendored
Normal file
21
.github/ISSUE_TEMPLATE.md
vendored
Normal 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
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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!
|
@ -57,7 +57,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.16.1",
|
"axios": "^0.16.1",
|
||||||
"dom-iterator": "^0.3.0",
|
|
||||||
"vue": "^2.3.3",
|
"vue": "^2.3.3",
|
||||||
"vue-electron": "^1.0.6",
|
"vue-electron": "^1.0.6",
|
||||||
"vuex": "^2.3.1"
|
"vuex": "^2.3.1"
|
||||||
|
57
src/editor/config.js
Normal file
57
src/editor/config.js
Normal 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
52
src/editor/index.js
Normal 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
686
src/editor/selection.js
Normal 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()
|
@ -1,5 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
paragraphClassName
|
emptyElementNames,
|
||||||
|
paragraphClassName,
|
||||||
|
blockContainerElementNames
|
||||||
} from './config.js'
|
} from './config.js'
|
||||||
/**
|
/**
|
||||||
* RegExp constants
|
* RegExp constants
|
||||||
@ -118,32 +120,127 @@ const checkLineBreakUpdate = text => {
|
|||||||
* viewModel2Html
|
* viewModel2Html
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const paragraph2Html = paph => {
|
const paragraph2Element = paph => {
|
||||||
const { id, paragraphType, markedText, cursorRange } = paph
|
const { id, paragraphType, markedText, cursorRange } = paph
|
||||||
let p = null
|
let element = null
|
||||||
switch (paragraphType) {
|
switch (paragraphType) {
|
||||||
case 'p':
|
case 'p':
|
||||||
p = document.createElement('p')
|
element = document.createElement('p')
|
||||||
p.id = id
|
element.id = id
|
||||||
p.classList.add(paragraphClassName)
|
element.classList.add(paragraphClassName)
|
||||||
p.innerHTML = markedText2Html(markedText, cursorRange)
|
element.innerHTML = markedText2Html(markedText, cursorRange)
|
||||||
break
|
break
|
||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
p.id = id
|
element.id = id
|
||||||
return p.outerHTML
|
return element
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewModel2Html = vm => {
|
const viewModel2Html = vm => {
|
||||||
const htmls = vm.map(p => paragraph2Html(p))
|
const htmls = vm.map(p => paragraph2Element(p).outerHTML)
|
||||||
return htmls.join('\n')
|
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 {
|
export {
|
||||||
getUniqueId,
|
getUniqueId,
|
||||||
markedText2Html,
|
markedText2Html,
|
||||||
checkInlineUpdate,
|
checkInlineUpdate,
|
||||||
checkLineBreakUpdate,
|
checkLineBreakUpdate,
|
||||||
viewModel2Html,
|
viewModel2Html,
|
||||||
paragraph2Html
|
paragraph2Element,
|
||||||
|
|
||||||
|
isBlockContainer,
|
||||||
|
traverseUp,
|
||||||
|
isAganippeEditorElement,
|
||||||
|
isElementAtBeginningOfBlock,
|
||||||
|
getFirstSelectableLeafNode,
|
||||||
|
findPreviousSibling,
|
||||||
|
getClosestBlockContainer
|
||||||
}
|
}
|
@ -4,7 +4,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Aganippe from '@/editor'
|
import Aganippe from '../../editor'
|
||||||
export default {
|
export default {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@ -24,6 +24,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@import '../themes/github.css';
|
@import '../../editor/themes/github.css';
|
||||||
@import '../editor/index.css';
|
@import '../../editor/index.css';
|
||||||
</style>
|
</style>
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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] 说明选择范围,如果相等,说明是光标位置。
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
Loading…
Reference in New Issue
Block a user