mirror of
https://github.com/marktext/marktext.git
synced 2025-05-04 04:21:38 +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;
|
padding: 1em 1em .05em 1em;
|
||||||
font-size: .8em;
|
font-size: .8em;
|
||||||
opacity: .8;
|
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 {
|
figure[data-role="FOOTNOTE"].ag-active::before {
|
||||||
@ -1155,11 +1159,26 @@ span.ag-reference-link {
|
|||||||
|
|
||||||
.ag-inline-footnote-identifier {
|
.ag-inline-footnote-identifier {
|
||||||
background: var(--codeBlockBgColor);
|
background: var(--codeBlockBgColor);
|
||||||
padding: 0em 0.3em;
|
padding: 0 0.4em;
|
||||||
border-radius: 1px;
|
border-radius: 3px;
|
||||||
font-size: .7em;
|
font-size: .7em;
|
||||||
|
color: var(--editorColor80);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-inline-footnote-identifier a {
|
.ag-inline-footnote-identifier a {
|
||||||
color: var(--editorColor);
|
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])
|
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 mathRender = target.closest(`.${CLASS_OR_ID.AG_MATH_RENDER}`)
|
||||||
const rubyRender = target.closest(`.${CLASS_OR_ID.AG_RUBY_RENDER}`)
|
const rubyRender = target.closest(`.${CLASS_OR_ID.AG_RUBY_RENDER}`)
|
||||||
const imageWrapper = target.closest(`.${CLASS_OR_ID.AG_INLINE_IMAGE}`)
|
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 imageDelete = target.closest('.ag-image-icon-delete') || target.closest('.ag-image-icon-close')
|
||||||
const mathText = mathRender && mathRender.previousElementSibling
|
const mathText = mathRender && mathRender.previousElementSibling
|
||||||
const rubyText = rubyRender && rubyRender.previousElementSibling
|
const rubyText = rubyRender && rubyRender.previousElementSibling
|
||||||
@ -125,6 +126,20 @@ class ClickEvent {
|
|||||||
return contentState.deleteImage(imageInfo)
|
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
|
// Handle image click, to select the current image
|
||||||
if (target.tagName === 'IMG' && imageWrapper) {
|
if (target.tagName === 'IMG' && imageWrapper) {
|
||||||
// Handle select image
|
// Handle select image
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { getLinkInfo } from '../utils/getLinkInfo'
|
import { getLinkInfo } from '../utils/getLinkInfo'
|
||||||
|
import { collectFootnotes } from '../utils'
|
||||||
|
|
||||||
class MouseEvent {
|
class MouseEvent {
|
||||||
constructor (muya) {
|
constructor (muya) {
|
||||||
@ -12,30 +13,61 @@ class MouseEvent {
|
|||||||
const handler = event => {
|
const handler = event => {
|
||||||
const target = event.target
|
const target = event.target
|
||||||
const parent = target.parentNode
|
const parent = target.parentNode
|
||||||
const { hideLinkPopup } = this.muya.options
|
const preSibling = target.previousElementSibling
|
||||||
if (!hideLinkPopup && parent && parent.tagName === 'A' && parent.classList.contains('ag-inline-rule')) {
|
const { hideLinkPopup, footnote } = this.muya.options
|
||||||
const rect = parent.getBoundingClientRect()
|
const rect = parent.getBoundingClientRect()
|
||||||
const reference = {
|
const reference = {
|
||||||
getBoundingClientRect () {
|
getBoundingClientRect () {
|
||||||
return rect
|
return rect
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hideLinkPopup && parent && parent.tagName === 'A' && parent.classList.contains('ag-inline-rule')) {
|
||||||
eventCenter.dispatch('muya-link-tools', {
|
eventCenter.dispatch('muya-link-tools', {
|
||||||
reference,
|
reference,
|
||||||
linkInfo: getLinkInfo(parent)
|
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 leaveHandler = event => {
|
||||||
const target = event.target
|
const target = event.target
|
||||||
const parent = target.parentNode
|
const parent = target.parentNode
|
||||||
|
const preSibling = target.previousElementSibling
|
||||||
|
const { footnote } = this.muya.options
|
||||||
if (parent && parent.tagName === 'A' && parent.classList.contains('ag-inline-rule')) {
|
if (parent && parent.tagName === 'A' && parent.classList.contains('ag-inline-rule')) {
|
||||||
eventCenter.dispatch('muya-link-tools', {
|
eventCenter.dispatch('muya-link-tools', {
|
||||||
reference: null
|
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)
|
eventCenter.attachDOMEvent(container, 'mouseover', handler)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { CLASS_OR_ID } from '../../../config'
|
import { CLASS_OR_ID } from '../../../config'
|
||||||
import { renderTableTools } from './renderToolBar'
|
import { renderTableTools } from './renderToolBar'
|
||||||
|
import { footnoteJumpIcon } from './renderFootnoteJump'
|
||||||
import { renderEditIcon } from './renderContainerEditIcon'
|
import { renderEditIcon } from './renderContainerEditIcon'
|
||||||
import { renderLeftBar, renderBottomBar } from './renderTableDargBar'
|
import { renderLeftBar, renderBottomBar } from './renderTableDargBar'
|
||||||
import { h } from '../snabbdom'
|
import { h } from '../snabbdom'
|
||||||
@ -126,8 +127,10 @@ export default function renderContainerBlock (parent, block, activeBlocks, match
|
|||||||
Object.assign(data.dataset, { role: functionType.toUpperCase() })
|
Object.assign(data.dataset, { role: functionType.toUpperCase() })
|
||||||
if (functionType === 'table') {
|
if (functionType === 'table') {
|
||||||
children.unshift(renderTableTools(activeBlocks))
|
children.unshift(renderTableTools(activeBlocks))
|
||||||
} else {
|
} else if (functionType !== 'footnote') {
|
||||||
children.unshift(renderEditIcon())
|
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)
|
const content = this.highlight(h, block, start + marker.length, end - 1, token)
|
||||||
|
|
||||||
return [
|
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(`span.${className}.${CLASS_OR_ID.AG_REMOVE}`, startMarker),
|
||||||
h('a', {
|
h('a', {
|
||||||
attrs: {
|
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
|
const { top, height } = rect
|
||||||
return (clientY - top) > (height / 2) ? 'down' : 'up'
|
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 Transformer from 'muya/lib/ui/transformer'
|
||||||
import FormatPicker from 'muya/lib/ui/formatPicker'
|
import FormatPicker from 'muya/lib/ui/formatPicker'
|
||||||
import LinkTools from 'muya/lib/ui/linkTools'
|
import LinkTools from 'muya/lib/ui/linkTools'
|
||||||
|
import FootnoteTool from 'muya/lib/ui/footnoteTool'
|
||||||
import TableBarTools from 'muya/lib/ui/tableTools'
|
import TableBarTools from 'muya/lib/ui/tableTools'
|
||||||
import FrontMenu from 'muya/lib/ui/frontMenu'
|
import FrontMenu from 'muya/lib/ui/frontMenu'
|
||||||
import Search from '../search'
|
import Search from '../search'
|
||||||
@ -470,6 +471,7 @@ export default {
|
|||||||
Muya.use(LinkTools, {
|
Muya.use(LinkTools, {
|
||||||
jumpClick: this.jumpClick
|
jumpClick: this.jumpClick
|
||||||
})
|
})
|
||||||
|
Muya.use(FootnoteTool)
|
||||||
Muya.use(TableBarTools)
|
Muya.use(TableBarTools)
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
|
Loading…
Reference in New Issue
Block a user