Create footnote from identifier and backlink

This commit is contained in:
罗冉 2019-10-27 23:52:23 +08:00
parent c7a2317eab
commit ec91b31d49
12 changed files with 323 additions and 14 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -186,7 +186,11 @@ figure[data-role="FOOTNOTE"] {
padding: 1em 1em .05em 1em;
font-size: .8em;
opacity: .8;
border-radius: 3px;
}
figure[data-role="FOOTNOTE"] .ag-paragraph-content:first-of-type:empty::after {
content: 'Input the footnote definition...';
color: var(--editorColor30);
}
figure[data-role="FOOTNOTE"].ag-active::before {
@ -1155,11 +1159,26 @@ span.ag-reference-link {
.ag-inline-footnote-identifier {
background: var(--codeBlockBgColor);
padding: 0em 0.3em;
border-radius: 1px;
padding: 0 0.4em;
border-radius: 3px;
font-size: .7em;
color: var(--editorColor80);
}
.ag-inline-footnote-identifier a {
color: var(--editorColor);
}
i.ag-footnote-backlink {
width: 20px;
height: 20px;
text-align: center;
line-height: 20px;
display: block;
position: absolute;
right: 1em;
bottom: .15em;
font-family: sans-serif;
cursor: pointer;
z-index: 100;
}

View File

@ -36,7 +36,27 @@ const footnoteCtrl = ContentState => {
this.checkInlineUpdate(pBlock.children[0])
}
return this.partialRender()
this.partialRender()
return sectionWrapper
}
ContentState.prototype.createFootnote = function (identifier) {
const { blocks } = this
const lastBlock = blocks[blocks.length - 1]
const newBlock = this.createBlockP(`[^${identifier}]: `)
this.insertAfter(newBlock, lastBlock)
const key = newBlock.children[0].key
const offset = newBlock.children[0].text.length
this.cursor = {
start: { key, offset },
end: { key, offset }
}
const sectionWrapper = this.updateFootnote(newBlock, newBlock.children[0])
const id = sectionWrapper.key
const footnoteEle = document.querySelector(`#${id}`)
if (footnoteEle) {
footnoteEle.scrollIntoView({ behavior: 'smooth' })
}
}
}

View File

@ -100,6 +100,7 @@ class ClickEvent {
const mathRender = target.closest(`.${CLASS_OR_ID.AG_MATH_RENDER}`)
const rubyRender = target.closest(`.${CLASS_OR_ID.AG_RUBY_RENDER}`)
const imageWrapper = target.closest(`.${CLASS_OR_ID.AG_INLINE_IMAGE}`)
const footnoteBackLink = target.closest('.ag-footnote-backlink')
const imageDelete = target.closest('.ag-image-icon-delete') || target.closest('.ag-image-icon-close')
const mathText = mathRender && mathRender.previousElementSibling
const rubyText = rubyRender && rubyRender.previousElementSibling
@ -125,6 +126,20 @@ class ClickEvent {
return contentState.deleteImage(imageInfo)
}
if (footnoteBackLink) {
event.preventDefault()
event.stopPropagation()
const figure = event.target.closest('figure')
const identifier = figure.querySelector('span.ag-footnote-input').textContent
if (identifier) {
const footnoteIdentifier = document.querySelector(`#noteref-${identifier}`)
if (footnoteIdentifier) {
footnoteIdentifier.scrollIntoView({ behavior: 'smooth' })
}
}
return
}
// Handle image click, to select the current image
if (target.tagName === 'IMG' && imageWrapper) {
// Handle select image

View File

@ -1,4 +1,5 @@
import { getLinkInfo } from '../utils/getLinkInfo'
import { collectFootnotes } from '../utils'
class MouseEvent {
constructor (muya) {
@ -12,30 +13,61 @@ class MouseEvent {
const handler = event => {
const target = event.target
const parent = target.parentNode
const { hideLinkPopup } = this.muya.options
if (!hideLinkPopup && parent && parent.tagName === 'A' && parent.classList.contains('ag-inline-rule')) {
const rect = parent.getBoundingClientRect()
const reference = {
getBoundingClientRect () {
return rect
}
const preSibling = target.previousElementSibling
const { hideLinkPopup, footnote } = this.muya.options
const rect = parent.getBoundingClientRect()
const reference = {
getBoundingClientRect () {
return rect
}
}
if (!hideLinkPopup && parent && parent.tagName === 'A' && parent.classList.contains('ag-inline-rule')) {
eventCenter.dispatch('muya-link-tools', {
reference,
linkInfo: getLinkInfo(parent)
})
}
if (
footnote &&
parent &&
parent.tagName === 'SUP' &&
parent.classList.contains('ag-inline-footnote-identifier') &&
preSibling &&
preSibling.classList.contains('ag-hide')
) {
const identifier = target.textContent
eventCenter.dispatch('muya-footnote-tool', {
reference,
identifier,
footnotes: collectFootnotes(this.muya.contentState.blocks)
})
}
}
const leaveHandler = event => {
const target = event.target
const parent = target.parentNode
const preSibling = target.previousElementSibling
const { footnote } = this.muya.options
if (parent && parent.tagName === 'A' && parent.classList.contains('ag-inline-rule')) {
eventCenter.dispatch('muya-link-tools', {
reference: null
})
}
if (
footnote &&
parent &&
parent.tagName === 'SUP' &&
parent.classList.contains('ag-inline-footnote-identifier') &&
preSibling &&
preSibling.classList.contains('ag-hide')
) {
eventCenter.dispatch('muya-footnote-tool', {
reference: null
})
}
}
eventCenter.attachDOMEvent(container, 'mouseover', handler)

View File

@ -1,5 +1,6 @@
import { CLASS_OR_ID } from '../../../config'
import { renderTableTools } from './renderToolBar'
import { footnoteJumpIcon } from './renderFootnoteJump'
import { renderEditIcon } from './renderContainerEditIcon'
import { renderLeftBar, renderBottomBar } from './renderTableDargBar'
import { h } from '../snabbdom'
@ -126,8 +127,10 @@ export default function renderContainerBlock (parent, block, activeBlocks, match
Object.assign(data.dataset, { role: functionType.toUpperCase() })
if (functionType === 'table') {
children.unshift(renderTableTools(activeBlocks))
} else {
} else if (functionType !== 'footnote') {
children.unshift(renderEditIcon())
} else {
children.push(footnoteJumpIcon())
}
}

View File

@ -0,0 +1,5 @@
import { h } from '../snabbdom'
export const footnoteJumpIcon = () => {
return h('i.ag-footnote-backlink', '↩︎')
}

View File

@ -10,7 +10,7 @@ export default function footnoteIdentifier (h, cursor, block, token, outerClass)
const content = this.highlight(h, block, start + marker.length, end - 1, token)
return [
h(`sup.${CLASS_OR_ID.AG_INLINE_FOOTNOTE_IDENTIFIER}.${CLASS_OR_ID.AG_INLINE_RULE}`, [
h(`sup#noteref-${token.content}.${CLASS_OR_ID.AG_INLINE_FOOTNOTE_IDENTIFIER}.${CLASS_OR_ID.AG_INLINE_RULE}`, [
h(`span.${className}.${CLASS_OR_ID.AG_REMOVE}`, startMarker),
h('a', {
attrs: {

View File

@ -0,0 +1,53 @@
.ag-footnote-tool-container {
width: 300px;
border-radius: 5px;
}
.ag-footnote-tool-container .ag-footnote-tool > div {
display: flex;
height: 35px;
align-items: center;
color: var(--editorColor);
font-size: 12px;
padding: 0 10px;
}
.ag-footnote-tool .text {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
flex: 1;
}
.ag-footnote-tool .btn {
width: 40px;
display: inline-block;
cursor: pointer;
}
.ag-footnote-tool .icon-wrapper {
width: 14px;
height: 14px;
margin-right: 5px;
position: relative;
}
.ag-footnote-tool .icon-wrapper i.icon {
display: inline-block;
position: absolute;
top: 0;
height: 100%;
width: 100%;
overflow: hidden;
color: var(--iconColor);
transition: all .25s ease-in-out;
}
.ag-footnote-tool .icon-wrapper i.icon > i[class^=icon-] {
display: inline-block;
width: 100%;
height: 100%;
filter: drop-shadow(14px 0 currentColor);
position: relative;
left: -14px;
}

View File

@ -0,0 +1,148 @@
import BaseFloat from '../baseFloat'
import { patch, h } from '../../parser/render/snabbdom'
import WarningIcon from '../../assets/pngicon/warning/2.png'
import './index.css'
const getFootnoteText = block => {
let text = ''
const travel = block => {
if (block.children.length === 0 && block.text) {
text += block.text
} else if (block.children.length) {
for (const b of block.children) {
travel(b)
}
}
}
const blocks = block.children.slice(1)
for (const b of blocks) {
travel(b)
}
return text
}
const defaultOptions = {
placement: 'bottom',
modifiers: {
offset: {
offset: '0, 5'
}
},
showArrow: false
}
class LinkTools extends BaseFloat {
static pluginName = 'footnoteTool'
constructor (muya, options = {}) {
const name = 'ag-footnote-tool'
const opts = Object.assign({}, defaultOptions, options)
super(muya, name, opts)
this.oldVnode = null
this.identifier = null
this.footnotes = null
this.options = opts
this.hideTimer = null
const toolContainer = this.toolContainer = document.createElement('div')
this.container.appendChild(toolContainer)
this.floatBox.classList.add('ag-footnote-tool-container')
this.listen()
}
listen () {
const { eventCenter } = this.muya
super.listen()
eventCenter.subscribe('muya-footnote-tool', ({ reference, identifier, footnotes }) => {
if (reference) {
this.footnotes = footnotes
this.identifier = identifier
setTimeout(() => {
this.show(reference)
this.render()
}, 0)
} else {
if (this.hideTimer) {
clearTimeout(this.hideTimer)
}
this.hideTimer = setTimeout(() => {
this.hide()
}, 500)
}
})
const mouseOverHandler = () => {
if (this.hideTimer) {
clearTimeout(this.hideTimer)
}
}
const mouseOutHandler = () => {
this.hide()
}
eventCenter.attachDOMEvent(this.container, 'mouseover', mouseOverHandler)
eventCenter.attachDOMEvent(this.container, 'mouseleave', mouseOutHandler)
}
render () {
const { oldVnode, toolContainer, identifier, footnotes } = this
const hasFootnote = footnotes.has(identifier)
const iconWrapperSelector = 'div.icon-wrapper'
const icon = h('i.icon', h('i.icon-inner', {
style: {
background: `url(${WarningIcon}) no-repeat`,
'background-size': '100%'
}
}, ''))
const iconWrapper = h(iconWrapperSelector, icon)
let text = 'Can\'t find footnote with syntax [^abc]:'
if (hasFootnote) {
const footnoteBlock = footnotes.get(identifier)
text = getFootnoteText(footnoteBlock)
if (!text) {
text = 'Input the footnote definition...'
}
}
const textNode = h('span.text', text)
const button = h('a.btn', {
on: {
click: event => {
this.buttonClick(event, hasFootnote)
}
}
}, hasFootnote ? 'Go to' : 'Create')
const children = [textNode, button]
if (!hasFootnote) {
children.unshift(iconWrapper)
}
const vnode = h('div', children)
if (oldVnode) {
patch(oldVnode, vnode)
} else {
patch(toolContainer, vnode)
}
this.oldVnode = vnode
}
buttonClick (event, hasFootnote) {
event.preventDefault()
event.stopPropagation()
const { identifier, footnotes } = this
if (hasFootnote) {
const block = footnotes.get(identifier)
const key = block.key
const ele = document.querySelector(`#${key}`)
ele.scrollIntoView({ behavior: 'smooth' })
} else {
this.muya.contentState.createFootnote(identifier)
}
return this.hide()
}
}
export default LinkTools

View File

@ -387,3 +387,15 @@ export const verticalPositionInRect = (event, rect) => {
const { top, height } = rect
return (clientY - top) > (height / 2) ? 'down' : 'up'
}
export const collectFootnotes = (blocks) => {
const map = new Map()
for (const block of blocks) {
if (block.type === 'figure' && block.functionType === 'footnote') {
const identifier = block.children[0].text
map.set(identifier, block)
}
}
return map
}

View File

@ -88,6 +88,7 @@ import ImageToolbar from 'muya/lib/ui/imageToolbar'
import Transformer from 'muya/lib/ui/transformer'
import FormatPicker from 'muya/lib/ui/formatPicker'
import LinkTools from 'muya/lib/ui/linkTools'
import FootnoteTool from 'muya/lib/ui/footnoteTool'
import TableBarTools from 'muya/lib/ui/tableTools'
import FrontMenu from 'muya/lib/ui/frontMenu'
import Search from '../search'
@ -470,6 +471,7 @@ export default {
Muya.use(LinkTools, {
jumpClick: this.jumpClick
})
Muya.use(FootnoteTool)
Muya.use(TableBarTools)
const options = {