link & window dialog for attributs (#59607)
This commit is contained in:
parent
e99c81194a
commit
849156cd3e
|
@ -11,7 +11,11 @@
|
|||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Menus */
|
||||
|
||||
/*
|
||||
* Menus
|
||||
*/
|
||||
|
||||
.menuicons {
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
|
@ -27,6 +31,9 @@
|
|||
color: inherit;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.menuicon {
|
||||
font-weight: 500;
|
||||
}
|
||||
.menuicon:hover {
|
||||
color: black;
|
||||
}
|
||||
|
@ -98,6 +105,7 @@
|
|||
color: white;
|
||||
border: 2px solid #888;
|
||||
border-radius: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
.godo-marks-menu .menuicon:first-child {
|
||||
border-radius: 3px 0 0 3px;
|
||||
|
@ -110,7 +118,7 @@
|
|||
}
|
||||
.godo-marks-menu .menuicon:disabled {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.menuicon-b {
|
||||
font-weight: 900;
|
||||
|
@ -123,8 +131,78 @@
|
|||
font-variant: small-caps;
|
||||
font-weight: 900;
|
||||
}
|
||||
.menuicon-a {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Menu dialog
|
||||
*/
|
||||
|
||||
/* dialog normalizer for polyfill */
|
||||
dialog {
|
||||
position: fixed;
|
||||
left: 0; right: 0;
|
||||
width: -moz-fit-content;
|
||||
width: -webkit-fit-content;
|
||||
width: fit-content;
|
||||
height: -moz-fit-content;
|
||||
height: -webkit-fit-content;
|
||||
height: fit-content;
|
||||
margin: auto;
|
||||
border: solid;
|
||||
padding: 1em;
|
||||
background: white;
|
||||
color: black;
|
||||
display: block;
|
||||
}
|
||||
dialog:not([open]) {
|
||||
display: none;
|
||||
}
|
||||
dialog::backdrop,
|
||||
dialog + .backdrop {
|
||||
position: fixed;
|
||||
top: 0; right: 0; bottom: 0; left: 0;
|
||||
background: rgba(0,0,0,0.3);
|
||||
}
|
||||
._dialog_overlay {
|
||||
position: fixed;
|
||||
top: 0; right: 0; bottom: 0; left: 0;
|
||||
}
|
||||
|
||||
.godo-dialog--menu {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: end;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.godo-dialog--menu button {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.godo-dialog--form p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.godo-dialog--form label {
|
||||
display: block;
|
||||
}
|
||||
.godo-dialog--form label > * {
|
||||
width: 0;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.godo-dialog--menuitem:not(:last-child) {
|
||||
margin-right: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Editor typo
|
||||
*/
|
||||
|
||||
/* Editor typo */
|
||||
div.godo .ProseMirror.godo--editor ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 1em;
|
||||
|
@ -133,3 +211,6 @@ div.godo .ProseMirror.godo--editor ul {
|
|||
div.godo .ProseMirror.godo--editor ul li p {
|
||||
margin: 0;
|
||||
}
|
||||
div.godo .ProseMirror.godo--editor a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dialog-polyfill": "^0.5.6",
|
||||
"prosemirror-commands": "^1.1.12",
|
||||
"prosemirror-history": "^1.2.0",
|
||||
"prosemirror-keymap": "^1.1.4",
|
||||
|
@ -265,6 +266,11 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dialog-polyfill": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/dialog-polyfill/-/dialog-polyfill-0.5.6.tgz",
|
||||
"integrity": "sha512-ZbVDJI9uvxPAKze6z146rmfUZjBqNEwcnFTVamQzXH+svluiV7swmVIGr7miwADgfgt1G2JQIytypM9fbyhX4w=="
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
|
@ -914,6 +920,11 @@
|
|||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
|
||||
"dev": true
|
||||
},
|
||||
"dialog-polyfill": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/dialog-polyfill/-/dialog-polyfill-0.5.6.tgz",
|
||||
"integrity": "sha512-ZbVDJI9uvxPAKze6z146rmfUZjBqNEwcnFTVamQzXH+svluiV7swmVIGr7miwADgfgt1G2JQIytypM9fbyhX4w=="
|
||||
},
|
||||
"escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"url": "https://git.entrouvert.org/godo.js.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"dialog-polyfill": "^0.5.6",
|
||||
"prosemirror-commands": "^1.1.12",
|
||||
"prosemirror-history": "^1.2.0",
|
||||
"prosemirror-keymap": "^1.1.4",
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
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
|
||||
|
@ -188,3 +191,77 @@ 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
import dialogPolyfill from 'dialog-polyfill'
|
||||
import getLanguage from './godo-menus-language.mjs'
|
||||
|
||||
const renderEl = () => {
|
||||
const tplString = `
|
||||
<dialog id="godo-dialog" class="godo-dialog">
|
||||
<h2 id="godo-dialog--title" class="godo-dialog--title"></h2>
|
||||
<form id="godo-dialog--form" class="godo-dialog--form" method="dialog">
|
||||
<div id="godo-dialog--content"></div>
|
||||
<menu class="godo-dialog--menu">
|
||||
<li class="godo-dialog--menuitem">
|
||||
<button id="godo-dialog--valid" class="godo-dialog--valid" value="validate">
|
||||
${getLanguage('ok').text}
|
||||
</button>
|
||||
</li>
|
||||
<li class="godo-dialog--menuitem">
|
||||
<button id="godo-dialog--remove" class="godo-dialog--remove" type="button" value="remove">
|
||||
${getLanguage('remove').text}
|
||||
</button>
|
||||
</li>
|
||||
<li class="godo-dialog--menuitem">
|
||||
<button id="godo-dialog--cancel" class="godo-dialog--cancel" type="button" value="cancel">
|
||||
${getLanguage('cancel').text}
|
||||
</button>
|
||||
</li>
|
||||
</menu>
|
||||
</form>
|
||||
</dialog>
|
||||
`
|
||||
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.innerHTML = tplString
|
||||
return wrapper.firstElementChild
|
||||
}
|
||||
|
||||
const dialog = renderEl()
|
||||
dialogPolyfill.registerDialog(dialog)
|
||||
|
||||
const titleEl = dialog.querySelector('#godo-dialog--title')
|
||||
const form = dialog.querySelector('#godo-dialog--form')
|
||||
const cancelBtn = dialog.querySelector('#godo-dialog--cancel')
|
||||
const removeBtn = dialog.querySelector('#godo-dialog--remove')
|
||||
const content = dialog.querySelector('#godo-dialog--content')
|
||||
|
||||
let fieldsStore
|
||||
let callbackStore
|
||||
|
||||
class TextField {
|
||||
constructor (options) {
|
||||
const defaultOptions = {
|
||||
name: '',
|
||||
label: 'label undefined',
|
||||
value: '',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
}
|
||||
this.options = Object.assign(defaultOptions, options)
|
||||
|
||||
if (!this.options.name) {
|
||||
console.error('TextField need a name')
|
||||
return
|
||||
}
|
||||
|
||||
return this.render()
|
||||
}
|
||||
render () {
|
||||
const o = this.options
|
||||
const tplString = `
|
||||
<label>
|
||||
${o.label}
|
||||
<input type="text"
|
||||
name="${o.name}"
|
||||
value="${o.value}"
|
||||
${o.required ? 'required' : ''}
|
||||
placeholder="${o.placeholder}">
|
||||
</input>
|
||||
</label>`
|
||||
let field = document.createElement('p')
|
||||
field.innerHTML = tplString
|
||||
return field
|
||||
}
|
||||
}
|
||||
|
||||
const openDialog = ({title, fields, callback}) => {
|
||||
fieldsStore = fields
|
||||
callbackStore = callback
|
||||
titleEl.innerText = title
|
||||
for (const field in fields) {
|
||||
content.appendChild(fields[field])
|
||||
}
|
||||
dialog.showModal()
|
||||
}
|
||||
|
||||
dialog.addEventListener('keydown', (e) => {
|
||||
if (dialog.open && e.key === 'Escape') {
|
||||
dialog.returnValue = 'cancel'
|
||||
}
|
||||
})
|
||||
|
||||
dialog.addEventListener('close', () => {
|
||||
const formdata = new FormData(form)
|
||||
let attrData = {}
|
||||
for (const field in fieldsStore) {
|
||||
attrData[field] = formdata.get(field)
|
||||
}
|
||||
callbackStore(dialog.returnValue, attrData)
|
||||
|
||||
content.innerHTML = ''
|
||||
titleEl.innerText = ''
|
||||
})
|
||||
|
||||
cancelBtn.addEventListener('click', () => {
|
||||
dialog.close('cancel')
|
||||
})
|
||||
|
||||
removeBtn.addEventListener('click', () => {
|
||||
dialog.close('remove')
|
||||
})
|
||||
|
||||
export {dialog, openDialog, TextField}
|
|
@ -9,10 +9,18 @@ const languageContent = {
|
|||
en: {
|
||||
icon: 'a',
|
||||
text: 'link',
|
||||
dialog: {
|
||||
title: 'Link parameter',
|
||||
href: 'Link'
|
||||
}
|
||||
},
|
||||
fr: {
|
||||
icon: 'a',
|
||||
text: 'lien',
|
||||
dialog: {
|
||||
title: 'Paramètre du lien',
|
||||
href: 'Lien'
|
||||
}
|
||||
},
|
||||
},
|
||||
'b': {
|
||||
|
@ -25,6 +33,14 @@ const languageContent = {
|
|||
text: 'gras',
|
||||
},
|
||||
},
|
||||
'cancel': {
|
||||
en: {
|
||||
text: 'cancel',
|
||||
},
|
||||
fr: {
|
||||
text: 'annuler',
|
||||
},
|
||||
},
|
||||
'h': {
|
||||
en: {
|
||||
icon: 'H',
|
||||
|
@ -65,6 +81,14 @@ const languageContent = {
|
|||
text: 'italique',
|
||||
},
|
||||
},
|
||||
'ok': {
|
||||
en: {
|
||||
text: 'validate',
|
||||
},
|
||||
fr: {
|
||||
text: 'valider',
|
||||
},
|
||||
},
|
||||
'p': {
|
||||
en: {
|
||||
icon: 'p',
|
||||
|
@ -75,6 +99,14 @@ const languageContent = {
|
|||
text: 'paragraphe',
|
||||
},
|
||||
},
|
||||
'remove': {
|
||||
en: {
|
||||
text: 'remove',
|
||||
},
|
||||
fr: {
|
||||
text: 'supprimer',
|
||||
},
|
||||
},
|
||||
'ul': {
|
||||
en: {
|
||||
icon: '•',
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {toggleMark, setBlockType, chainCommands} from 'prosemirror-commands'
|
||||
import {wrapInList, liftListItem} from './godo-additional-commands.mjs'
|
||||
import {wrapInList, liftListItem, editOrToggleLink} from './godo-additional-commands.mjs'
|
||||
import getLanguage from './godo-menus-language.mjs'
|
||||
|
||||
class Menu {
|
||||
|
@ -19,7 +19,7 @@ class Menu {
|
|||
|
||||
this.update(editorView, null)
|
||||
|
||||
this.el.addEventListener('mousedown', e => {
|
||||
this.el.addEventListener('mouseup', e => {
|
||||
e.preventDefault()
|
||||
for (const item in this.items) {
|
||||
const {dom, run} = this.items[item]
|
||||
|
@ -110,18 +110,6 @@ class Menu {
|
|||
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}),
|
||||
|
@ -189,7 +177,7 @@ function marks (schema) {
|
|||
|
||||
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)
|
||||
if ((type = schema.marks.link)) { i.toggleLink = {run: editOrToggleLink(type), dom: icon('a'), type} }
|
||||
|
||||
menu.items = i
|
||||
return menu
|
||||
|
|
|
@ -38,6 +38,14 @@ 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] },
|
||||
},
|
||||
// :: MarkSpec An emphasis mark. Rendered as an `<em>` element.
|
||||
// Has parse rules that also match `<i>` and `font-style: italic`.
|
||||
em: {
|
||||
|
@ -100,22 +108,6 @@ const listSchemaContent = {
|
|||
},
|
||||
},
|
||||
}
|
||||
const linkSchemaContent = { // eslint-disable-line no-unused-vars
|
||||
marks: {
|
||||
link: {
|
||||
attrs: {
|
||||
href: {},
|
||||
title: {default: null},
|
||||
},
|
||||
inclusive: false,
|
||||
parseDOM: [{tag: 'a[href]',
|
||||
getAttrs (dom) {
|
||||
return {href: dom.getAttribute('href'), title: dom.getAttribute('title')}
|
||||
}}],
|
||||
toDOM (node) { let {href, title} = node.attrs; return ['a', {href, title}, 0] },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const basicSchema = new Schema(basicSchemaContent)
|
||||
|
||||
|
@ -126,8 +118,8 @@ const fullSchema = new Schema({
|
|||
listSchemaContent.nodes,
|
||||
),
|
||||
marks: Object.assign({},
|
||||
basicSchemaContent.marks
|
||||
)
|
||||
});
|
||||
basicSchemaContent.marks,
|
||||
),
|
||||
})
|
||||
|
||||
export { basicSchema, fullSchema }
|
||||
|
|
|
@ -9,6 +9,7 @@ import {DOMParser, DOMSerializer} from 'prosemirror-model'
|
|||
import {buildKeymap} from './godo-additional-keymap.mjs'
|
||||
import {basicSchema, fullSchema} from './godo-schemas.mjs'
|
||||
import {Menu, blocks, marks} from './godo-menus.mjs'
|
||||
import {dialog} from './godo-dialog.mjs'
|
||||
|
||||
function menuPlugin (menu) {
|
||||
return new Plugin({
|
||||
|
@ -51,6 +52,7 @@ export default class Godo {
|
|||
|
||||
this.marksMenu = menuPlugin(marks(this.schema))
|
||||
this.blocksMenu = menuPlugin(blocks(this.schema, this.options.headingLevels))
|
||||
this.menuDialog = document.body.appendChild(dialog)
|
||||
|
||||
let pluginsList = [
|
||||
history(),
|
||||
|
|
Loading…
Reference in New Issue