mirror of
https://github.com/marktext/marktext.git
synced 2025-05-03 03:31:29 +08:00
Create footnote from identifier and backlink
This commit is contained in:
parent
c7a2317eab
commit
ec91b31d49
BIN
src/muya/lib/assets/pngicon/warning/2.png
Normal file
BIN
src/muya/lib/assets/pngicon/warning/2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.9 KiB |
@ -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;
|
||||
}
|
||||
|
@ -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' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
import { h } from '../snabbdom'
|
||||
|
||||
export const footnoteJumpIcon = () => {
|
||||
return h('i.ag-footnote-backlink', '↩︎')
|
||||
}
|
@ -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: {
|
||||
|
53
src/muya/lib/ui/footnoteTool/index.css
Normal file
53
src/muya/lib/ui/footnoteTool/index.css
Normal 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;
|
||||
}
|
148
src/muya/lib/ui/footnoteTool/index.js
Normal file
148
src/muya/lib/ui/footnoteTool/index.js
Normal 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
|
@ -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
|
||||
}
|
||||
|
@ -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 = {
|
||||
|
Loading…
Reference in New Issue
Block a user