link & window dialog for attributs (#59607)

This commit is contained in:
Thomas Jund 2022-05-30 10:01:55 +02:00
parent e99c81194a
commit 849156cd3e
9 changed files with 341 additions and 37 deletions

87
dist/css/godo.css vendored
View File

@ -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;
}

11
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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
}
}

120
src-js/godo-dialog.mjs Normal file
View File

@ -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}

View File

@ -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: '•',

View File

@ -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

View File

@ -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 }

View File

@ -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(),