199 lines
5.2 KiB
JavaScript
199 lines
5.2 KiB
JavaScript
import {toggleMark, setBlockType, chainCommands} from 'prosemirror-commands'
|
|
import {wrapInList, liftListItem} from './godo-additional-commands.mjs'
|
|
import getLanguage from './godo-menus-language.mjs'
|
|
|
|
class Menu {
|
|
constructor (menu, editorView) {
|
|
this.items = menu.items
|
|
this.editorView = editorView
|
|
this.name = menu.name
|
|
|
|
this.el = document.createElement('div')
|
|
this.el.className = `godo-${this.name}-menu`
|
|
this.el.wrapper = document.createElement('div')
|
|
this.el.wrapper.className = 'menuicons'
|
|
for (const item in this.items) {
|
|
this.el.wrapper.appendChild(this.items[item].dom)
|
|
}
|
|
this.el.appendChild(this.el.wrapper)
|
|
|
|
this.update(editorView, null)
|
|
|
|
this.el.addEventListener('mousedown', e => {
|
|
e.preventDefault()
|
|
for (const item in this.items) {
|
|
const {dom, run} = this.items[item]
|
|
if (dom.contains(e.target)) {
|
|
run(editorView.state, editorView.dispatch, editorView)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
update (view, lastState) {
|
|
let state = view.state
|
|
|
|
if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
|
|
return
|
|
}
|
|
|
|
if (this.name === 'marks') {
|
|
this.updateMarks(view, state)
|
|
}
|
|
|
|
if (this.name === 'blocks') {
|
|
this.udpateBlocks(view, state)
|
|
}
|
|
}
|
|
|
|
setMenuItem (menuItem, isActive, isDisabled = true) {
|
|
if (isActive) {
|
|
menuItem.classList.add('active')
|
|
} else {
|
|
menuItem.classList.remove('active')
|
|
}
|
|
menuItem.disabled = isDisabled
|
|
}
|
|
|
|
udpateBlocks (view, state) {
|
|
for (const item in this.items) {
|
|
const {dom, run, type, active, options} = this.items[item]
|
|
const activeDefault = () => {
|
|
let {$from, to} = state.selection
|
|
return to <= $from.end() && $from.parent.hasMarkup(type, options)
|
|
}
|
|
const isActive = (active) ? active(state) : activeDefault()
|
|
const isDisabled = !run(state, null, view)
|
|
this.setMenuItem(dom, isActive, isDisabled)
|
|
}
|
|
}
|
|
|
|
updateMarks (view, state) {
|
|
let $sel = state.selection
|
|
const $cursor = $sel.$cursor
|
|
const ranges = $sel.ranges
|
|
let getIsActive
|
|
|
|
// if single cursor
|
|
if ($cursor && $cursor.marks()) {
|
|
const activeMarks = $cursor.marks().map(m => m.type.name)
|
|
getIsActive = (type) => activeMarks.includes(type.name)
|
|
this.el.classList.add('fade')
|
|
|
|
// else = select range
|
|
} else {
|
|
const range = ranges[0]
|
|
if (range && ranges.length === 1){
|
|
getIsActive = (type) => state.doc.rangeHasMark(range.$from.pos, range.$to.pos, type)
|
|
this.el.classList.remove('fade')
|
|
}
|
|
}
|
|
|
|
for (const item in this.items) {
|
|
const {dom, type, run} = this.items[item]
|
|
const isActive = getIsActive(type)
|
|
let isDisabled = !run(state, null, view)
|
|
this.setMenuItem(dom, isActive, isDisabled)
|
|
}
|
|
|
|
// move marks nav above selection
|
|
let {from, to} = $sel
|
|
let start = view.coordsAtPos(from)
|
|
let end = view.coordsAtPos(to)
|
|
let left = Math.max((start.left + end.left) / 2, start.left + 3)
|
|
this.el.style.transform = `translate(${left}px, ${end.bottom}px)`
|
|
|
|
// if (view.hasFocus()) this.el.hidden = false;
|
|
this.el.hidden = view.hasFocus()
|
|
}
|
|
|
|
destroy () { this.el.remove() }
|
|
}
|
|
|
|
|
|
function linkItem (type) {
|
|
return {
|
|
dom: icon('a'),
|
|
run (state, dispatch) {
|
|
let chref = prompt('href')
|
|
toggleMark(type, {href: chref})(state, dispatch)
|
|
},
|
|
type,
|
|
}
|
|
}
|
|
|
|
function setHeader (type, level, iconId) {
|
|
return {
|
|
run: setBlockType(type, {level}),
|
|
dom: icon(iconId),
|
|
type,
|
|
options: {level},
|
|
}
|
|
}
|
|
|
|
// Helper function to create menu icons
|
|
// id: defined in languageContent
|
|
function icon (id) {
|
|
let menuicon = document.createElement('button')
|
|
menuicon.className = 'menuicon menuicon-' + id
|
|
menuicon.setAttribute('type', 'button')
|
|
menuicon.title = getLanguage(id).text
|
|
menuicon.textContent = getLanguage(id).icon
|
|
return menuicon
|
|
}
|
|
|
|
function blocks (schema, hLevels) {
|
|
let menu = {
|
|
name: 'blocks',
|
|
hLevels,
|
|
}
|
|
let type; let i = {}
|
|
|
|
if ((type = schema.nodes.heading)) {
|
|
let iconId = 'h'
|
|
hLevels.forEach((level) => {
|
|
i['setHeader' + level] = setHeader(type, level, iconId)
|
|
iconId = iconId + 'h'
|
|
})
|
|
}
|
|
if ((type = schema.nodes.paragraph)) {
|
|
i.setP = {run: setBlockType(type), dom: icon('p'), type}
|
|
}
|
|
if ((type = schema.nodes.bullet_list)) {
|
|
i.setList = {
|
|
run: chainCommands(wrapInList(type), liftListItem(schema.nodes.list_item)),
|
|
dom: icon('ul'),
|
|
type,
|
|
active (state) {
|
|
let {$from, $to} = state.selection
|
|
let range = $from.blockRange($to)
|
|
switch (range.parent.type) {
|
|
case type:
|
|
case schema.nodes.list_item:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
},
|
|
}
|
|
}
|
|
menu.items = i
|
|
return menu
|
|
}
|
|
|
|
function marks (schema) {
|
|
let menu = {
|
|
name: 'marks',
|
|
}
|
|
let type; let i = {}
|
|
|
|
if ((type = schema.marks.strong)) i.toggleStrong = {run: toggleMark(type), dom: icon('b'), type}
|
|
if ((type = schema.marks.em)) i.toggleEm = {run: toggleMark(type), dom: icon('i'), type}
|
|
if ((type = schema.marks.link)) i.toggleLink = linkItem(type)
|
|
|
|
menu.items = i
|
|
return menu
|
|
}
|
|
|
|
export { Menu, blocks, marks }
|