js: separate link mark in a component and test it (#69407)
This commit is contained in:
parent
693c85baa9
commit
83da849bb9
|
@ -0,0 +1,106 @@
|
|||
import {toggleMark} from 'prosemirror-commands'
|
||||
import {icon} from '../utils.mjs'
|
||||
|
||||
import { openDialog, TextField } from '../godo-dialog.mjs'
|
||||
import getLanguage from '../godo-menus-language.mjs'
|
||||
|
||||
export default () => ({
|
||||
marks: {
|
||||
// :: MarkSpec A link mark. `href` attribute.
|
||||
// Rendered and parsed as an `<a>` element.
|
||||
link: {
|
||||
attrs: {href: {}},
|
||||
inclusive: false,
|
||||
parseDOM: [{
|
||||
tag: 'a[href]',
|
||||
getAttrs: (node) => ({href: node.href}),
|
||||
}],
|
||||
toDOM: (node) => ['a', {href: node.attrs.href}, 0],
|
||||
},
|
||||
},
|
||||
marksMenu (schema) {
|
||||
const link = schema.marks.link
|
||||
return {
|
||||
toggleLink: {
|
||||
run: editOrToggleLink(link),
|
||||
dom: icon('a'),
|
||||
type: link,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
// Edit or Toggle Link. For range only.
|
||||
// create link or Edit link if link exist in select range
|
||||
// or remove link if multiple links exists in select range
|
||||
function editOrToggleLink (linkType) {
|
||||
return function (state, dispatch, view) {
|
||||
let tr = state.tr
|
||||
let $sel = state.selection
|
||||
let range = $sel.ranges[0]
|
||||
if (!range && $sel.ranges.length > 1) return
|
||||
|
||||
function setLink (linkNodes, attrs) {
|
||||
linkNodes.forEach(linkNode => {
|
||||
tr.addMark(linkNode.from, linkNode.to, linkType.create(attrs))
|
||||
})
|
||||
dispatch(tr)
|
||||
}
|
||||
|
||||
function unsetLink (linkNodes) {
|
||||
linkNodes.forEach(linkNode => {
|
||||
tr.removeMark(linkNode.from, linkNode.to, linkType)
|
||||
})
|
||||
dispatch(tr)
|
||||
}
|
||||
|
||||
if (dispatch) {
|
||||
const {from, to} = $sel
|
||||
let linkNodes = []
|
||||
let linkTargets = new Set()
|
||||
state.doc.nodesBetween(from, to, (node, pos) => {
|
||||
const linkMarks = node.marks.filter(m => linkType === m.type)
|
||||
if (linkMarks.length) {
|
||||
linkNodes.push({
|
||||
node,
|
||||
from: pos,
|
||||
to: pos + node.nodeSize,
|
||||
})
|
||||
linkMarks.forEach(m => linkTargets.add(m.attrs.href))
|
||||
}
|
||||
})
|
||||
|
||||
if (linkTargets.size > 1) {
|
||||
unsetLink(linkNodes)
|
||||
} else {
|
||||
openDialog({
|
||||
title: getLanguage('a').dialog.title,
|
||||
fields: {
|
||||
href: new TextField({
|
||||
name: 'href',
|
||||
label: getLanguage('a').dialog.href,
|
||||
required: true,
|
||||
value: linkTargets.values().next().value || '', // get first element of set or empty string
|
||||
}),
|
||||
},
|
||||
callback (dialogValue, attrs) {
|
||||
switch (dialogValue) {
|
||||
case 'validate':
|
||||
if (linkNodes.length) setLink(linkNodes, attrs)
|
||||
else toggleMark(linkType, attrs)(state, dispatch)
|
||||
break
|
||||
case 'remove':
|
||||
unsetLink(linkNodes)
|
||||
break
|
||||
}
|
||||
view.focus()
|
||||
},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return toggleMark(linkType)(state, null)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,8 +1,5 @@
|
|||
import {findWrapping, liftTarget, canSplit, ReplaceAroundStep} from 'prosemirror-transform'
|
||||
import {Slice, Fragment, NodeRange} from 'prosemirror-model'
|
||||
import {toggleMark} from 'prosemirror-commands'
|
||||
import {openDialog, TextField} from './godo-dialog.mjs'
|
||||
import getLanguage from './godo-menus-language.mjs'
|
||||
|
||||
|
||||
// wrapInList, doWrapInList, splitListItem, liftToOuterList, liftOutOfList, sinkListItem
|
||||
|
@ -191,77 +188,3 @@ export function sinkListItem (itemType) {
|
|||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Edit or Toggle Link. For range only.
|
||||
// create link or Edit link if link exist in select range
|
||||
// or remove link if multiple links exists in select range
|
||||
export function editOrToggleLink (linkType) {
|
||||
return function (state, dispatch, view) {
|
||||
let tr = state.tr
|
||||
let $sel = state.selection
|
||||
let range = $sel.ranges[0]
|
||||
if (!range && $sel.ranges.length > 1) return
|
||||
|
||||
function setLink (linkNodes, attrs) {
|
||||
linkNodes.forEach(linkNode => {
|
||||
tr.addMark(linkNode.from, linkNode.to, linkType.create(attrs))
|
||||
})
|
||||
dispatch(tr)
|
||||
}
|
||||
|
||||
function unsetLink (linkNodes) {
|
||||
linkNodes.forEach(linkNode => {
|
||||
tr.removeMark(linkNode.from, linkNode.to, linkType)
|
||||
})
|
||||
dispatch(tr)
|
||||
}
|
||||
|
||||
if (dispatch) {
|
||||
const {from, to} = $sel
|
||||
let linkNodes = []
|
||||
let linkTargets = new Set()
|
||||
state.doc.nodesBetween(from, to, (node, pos) => {
|
||||
const linkMarks = node.marks.filter(m => linkType === m.type)
|
||||
if (linkMarks.length) {
|
||||
linkNodes.push({
|
||||
node,
|
||||
from: pos,
|
||||
to: pos + node.nodeSize,
|
||||
})
|
||||
linkMarks.forEach(m => linkTargets.add(m.attrs.href))
|
||||
}
|
||||
})
|
||||
|
||||
if (linkTargets.size > 1) {
|
||||
unsetLink(linkNodes)
|
||||
} else {
|
||||
openDialog({
|
||||
title: getLanguage('a').dialog.title,
|
||||
fields: {
|
||||
href: new TextField({
|
||||
name: 'href',
|
||||
label: getLanguage('a').dialog.href,
|
||||
required: true,
|
||||
value: linkTargets.values().next().value || '', // get first element of set or empty string
|
||||
}),
|
||||
},
|
||||
callback (dialogValue, attrs) {
|
||||
switch (dialogValue) {
|
||||
case 'validate':
|
||||
if (linkNodes.length) setLink(linkNodes, attrs)
|
||||
else toggleMark(linkType, attrs)(state, dispatch)
|
||||
break
|
||||
case 'remove':
|
||||
unsetLink(linkNodes)
|
||||
break
|
||||
}
|
||||
view.focus()
|
||||
},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return toggleMark(linkType)(state, null)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {chainCommands} from 'prosemirror-commands'
|
||||
import {wrapInList, liftListItem, editOrToggleLink} from './godo-additional-commands.mjs'
|
||||
import {wrapInList, liftListItem} from './godo-additional-commands.mjs'
|
||||
import {icon} from './utils.mjs'
|
||||
|
||||
class Menu {
|
||||
|
@ -150,18 +150,15 @@ function blocks (components, schema) {
|
|||
function marks (components, schema) {
|
||||
let menu = {
|
||||
name: 'marks',
|
||||
items: {},
|
||||
}
|
||||
let type; let i = {}
|
||||
|
||||
for (const component of components) {
|
||||
if (component.marksMenu) {
|
||||
Object.assign(i, component.marksMenu(schema))
|
||||
Object.assign(menu.items, component.marksMenu(schema))
|
||||
}
|
||||
}
|
||||
|
||||
if ((type = schema.marks.link)) { i.toggleLink = {run: editOrToggleLink(type), dom: icon('a'), type} }
|
||||
|
||||
menu.items = i
|
||||
return menu
|
||||
}
|
||||
|
||||
|
|
|
@ -7,20 +7,6 @@
|
|||
// To reuse elements from this schema, extend or read from its
|
||||
// `spec.nodes` and `spec.marks` [properties](#model.Schema.spec).
|
||||
|
||||
// BASIC
|
||||
const basicSchemaContent = {
|
||||
marks: {
|
||||
// :: MarkSpec A link mark. `href` attribute.
|
||||
// Rendered and parsed as an `<a>` element.
|
||||
link: {
|
||||
attrs: {href: {}},
|
||||
inclusive: false,
|
||||
parseDOM: [{tag: 'a[href]', getAttrs (node) { return {href: node.href} }}],
|
||||
toDOM (node) { return ['a', {href: node.attrs.href}, 0] },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// LISTS
|
||||
const listSchemaContent = {
|
||||
nodes: {
|
||||
|
@ -41,16 +27,10 @@ const listSchemaContent = {
|
|||
},
|
||||
}
|
||||
|
||||
const basicSchema = basicSchemaContent
|
||||
|
||||
const fullSchema = {
|
||||
nodes: Object.assign({},
|
||||
basicSchemaContent.nodes,
|
||||
listSchemaContent.nodes,
|
||||
),
|
||||
marks: Object.assign({},
|
||||
basicSchemaContent.marks,
|
||||
),
|
||||
}
|
||||
|
||||
export { basicSchema, fullSchema }
|
||||
export { fullSchema }
|
||||
|
|
|
@ -6,13 +6,14 @@ import {baseKeymap} from 'prosemirror-commands'
|
|||
import {DOMParser, DOMSerializer} from 'prosemirror-model'
|
||||
|
||||
import {buildKeymap} from './godo-additional-keymap.mjs'
|
||||
import {basicSchema, fullSchema} from './godo-schemas.mjs'
|
||||
import {fullSchema} from './godo-schemas.mjs'
|
||||
import {Menu, blocks, marks} from './godo-menus.mjs'
|
||||
import {dialog} from './godo-dialog.mjs'
|
||||
import {loadSchema} from './utils.mjs'
|
||||
import base from './components/base.mjs'
|
||||
import fontMarks from './components/font-marks.mjs'
|
||||
import heading from './components/heading.mjs'
|
||||
import link from './components/link.mjs'
|
||||
|
||||
function menuPlugin (menu) {
|
||||
return new Plugin({
|
||||
|
@ -61,14 +62,14 @@ export default class Godo {
|
|||
switch (this.options.schema) {
|
||||
case 'basic':
|
||||
displayBlocksMenu = false
|
||||
return basicSchema
|
||||
return {}
|
||||
case 'full':
|
||||
default:
|
||||
return fullSchema
|
||||
}
|
||||
})()
|
||||
|
||||
const componentsDefinitions = [base, fontMarks]
|
||||
const componentsDefinitions = [base, fontMarks, link]
|
||||
if (this.options.schema === 'full') {
|
||||
componentsDefinitions.push(heading)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
env:
|
||||
node: true
|
||||
globals:
|
||||
fail: readonly # Jasmine's 'fail', Jasmine is used by jest
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
import {test, expect, vi} from 'vitest'
|
||||
import {Node} from 'prosemirror-model'
|
||||
|
||||
import {loadSchema} from '../../src-js/utils'
|
||||
import {parse, serialize, TestState} from '../helpers'
|
||||
|
||||
import base from '../../src-js/components/base'
|
||||
|
||||
const openDialogMock = vi.fn()
|
||||
vi.mock('../../src-js/godo-dialog.mjs', () => ({
|
||||
TextField: vi.fn(),
|
||||
openDialog: openDialogMock,
|
||||
}))
|
||||
|
||||
test('parse dom', async () => {
|
||||
const {default: link} = await import('../../src-js/components/link')
|
||||
const schema = loadSchema([base(), link()])
|
||||
const node = parse(schema, '<p><a href="https://entrouvert.com">CLIQUE</a></p>')
|
||||
expect(node.toJSON()).toEqual({
|
||||
type: 'doc',
|
||||
content: [{
|
||||
type: 'paragraph',
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: 'CLIQUE',
|
||||
marks: [{
|
||||
type: 'link',
|
||||
attrs: { href: 'https://entrouvert.com/' },
|
||||
}],
|
||||
}],
|
||||
}],
|
||||
})
|
||||
})
|
||||
|
||||
test('serialize to dom', async () => {
|
||||
const {default: link} = await import('../../src-js/components/link')
|
||||
const schema = loadSchema([base(), link()])
|
||||
const doc = Node.fromJSON(schema, {
|
||||
type: 'doc',
|
||||
content: [{
|
||||
type: 'paragraph',
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: 'CLIQUE',
|
||||
marks: [{
|
||||
type: 'link',
|
||||
attrs: { href: 'https://entrouvert.com' },
|
||||
}],
|
||||
}],
|
||||
}],
|
||||
})
|
||||
|
||||
expect(serialize(schema, doc)).toBe('<p><a href="https://entrouvert.com">CLIQUE</a></p>')
|
||||
})
|
||||
|
||||
test('marks menu actions', async () => {
|
||||
const {default: link} = await import('../../src-js/components/link')
|
||||
const state = new TestState([base(), link()], '<p>CLIQUE SAPRISTI</p>')
|
||||
|
||||
function mockValidate (url) {
|
||||
openDialogMock.mockImplementation(({callback}) => {
|
||||
callback('validate', {href: url})
|
||||
})
|
||||
}
|
||||
|
||||
function mockRemove () {
|
||||
openDialogMock.mockImplementation(({callback}) => {
|
||||
callback('remove')
|
||||
})
|
||||
}
|
||||
|
||||
state.select('CLIQUE')
|
||||
mockValidate('https://clique.com')
|
||||
state.clickMarksMenu('toggleLink')
|
||||
expect(state.html).toBe('<p><a href="https://clique.com">CLIQUE</a> SAPRISTI</p>')
|
||||
|
||||
state.select('CLIQUE')
|
||||
mockRemove()
|
||||
state.clickMarksMenu('toggleLink')
|
||||
expect(state.html).toBe('<p>CLIQUE SAPRISTI</p>')
|
||||
|
||||
state.select('CLIQUE')
|
||||
mockValidate('https://clique.com')
|
||||
state.clickMarksMenu('toggleLink')
|
||||
state.select('SAPRISTI')
|
||||
mockValidate('https://clique-encore.com')
|
||||
state.clickMarksMenu('toggleLink')
|
||||
expect(state.html).toBe(
|
||||
'<p><a href="https://clique.com">CLIQUE</a> '
|
||||
+ '<a href="https://clique-encore.com">SAPRISTI</a></p>',
|
||||
)
|
||||
|
||||
state.select('LIQUE SAPRI')
|
||||
openDialogMock.mockImplementation(() => fail())
|
||||
state.clickMarksMenu('toggleLink')
|
||||
expect(state.html).toBe('<p>CLIQUE SAPRISTI</p>')
|
||||
})
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import {EditorState, Selection, TextSelection} from 'prosemirror-state'
|
||||
import {DOMParser, DOMSerializer} from 'prosemirror-model'
|
||||
import {history} from 'prosemirror-history'
|
||||
import {vi} from 'vitest'
|
||||
|
||||
import {loadSchema} from '../src-js/utils'
|
||||
import {buildKeymap} from '../src-js/godo-additional-keymap.mjs'
|
||||
|
@ -22,6 +23,10 @@ export function serialize (schema, fragment) {
|
|||
return div.innerHTML
|
||||
}
|
||||
|
||||
const viewMock = {
|
||||
focus: vi.fn(),
|
||||
}
|
||||
|
||||
export class TestState {
|
||||
constructor (components, initialHtml) {
|
||||
const schema = loadSchema(components)
|
||||
|
@ -85,5 +90,5 @@ export class TestState {
|
|||
|
||||
clickBlocksMenu (name) { this.blocksMenu[name].run(this.state, tr => this.apply(tr)) }
|
||||
|
||||
clickMarksMenu (name) { this.marksMenu[name].run(this.state, tr => this.apply(tr)) }
|
||||
clickMarksMenu (name) { this.marksMenu[name].run(this.state, tr => this.apply(tr), viewMock) }
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue