diff --git a/lang/de/lang.php b/lang/de/lang.php index 346db05f..cc5517aa 100644 --- a/lang/de/lang.php +++ b/lang/de/lang.php @@ -100,4 +100,6 @@ $lang['js']['label:rss details'] = 'Zeige die Beschreibung des Eintrages'; $lang['js']['label:rss refresh'] = 'Abfragerhythmus'; +$lang['js']['button:insert paragraph'] = 'Absatz einfügen'; + $lang['js']['code_block_hint'] = '💡 Zum Verlassen drücken Sie STRG+Enter'; diff --git a/lang/en/lang.php b/lang/en/lang.php index 2ef69a33..1e8e9b8a 100644 --- a/lang/en/lang.php +++ b/lang/en/lang.php @@ -100,4 +100,6 @@ $lang['js']['label:rss details'] = 'Show the item description'; $lang['js']['label:rss refresh'] = 'Refresh period'; +$lang['js']['button:insert paragraph'] = 'Insert paragraph'; + $lang['js']['code_block_hint'] = '💡 Press CTRL+Enter to exit'; diff --git a/script/custom/paragraphButtons.js b/script/custom/paragraphButtons.js new file mode 100644 index 00000000..8d603196 --- /dev/null +++ b/script/custom/paragraphButtons.js @@ -0,0 +1,49 @@ +import { insertParagraphAtPos } from '../customCommands'; + +/** + * Attach event handler to buttons for inserting a paragraph + * + * @return {void} + */ +export function initializeButtons() { + if (window.Prosemirror.paragraphHandlingIsInitialized) { + return; + } + window.Prosemirror.paragraphHandlingIsInitialized = true; + jQuery(document).on('click', '.ProseMirror button.addParagraphButton', function insertParagraph(event) { + event.stopPropagation(); + event.preventDefault(); + const $button = jQuery(this); + const viewID = $button.data('viewid'); + const direction = $button.data('direction'); + const view = window.Prosemirror.views[viewID]; + + const pos = view.posAtDOM(event.target.parentNode); + const command = insertParagraphAtPos(pos, direction); + command(view.state, view.dispatch); + }); +} + +/** + * Produce the spec for a button to insert a paragraph before or after the respective element + * + * @param viewID + * @param direction + * @return {array} + */ +export function getButtonSpec(viewID, direction) { + if (typeof LANG === 'undefined') { + // early return during js schema testing + return []; + } + return [ + 'button', + { + class: 'addParagraphButton', + type: 'button', + 'data-direction': direction, + 'data-viewid': viewID, + }, + LANG.plugins.prosemirror['button:insert paragraph'], + ]; +} diff --git a/script/customCommands.js b/script/customCommands.js index 9602a8a7..141223dc 100644 --- a/script/customCommands.js +++ b/script/customCommands.js @@ -1,3 +1,5 @@ +import { Selection } from 'prosemirror-state'; + /** * Returns a command that tries to set the selected textblocks to the given node type with the given attributes. * @@ -20,3 +22,31 @@ export function setBlockTypeNoAttrCheck(nodeType, attrs) { // eslint-disable-lin return true; }; } + +/** + * Returns a command that inserts a new paragraph before or after the node at the given position + * + * @param {int} pos position near which the paragraph should be inserted + * @param {string} direction should be 'before' or 'after' + * @return {function} + */ +export function insertParagraphAtPos(pos, direction = 'after') { + return function insertParagraphAtPosCommand(state, dispatch) { + const $pos = state.doc.resolve(pos); + const above = $pos.node(-1); + const beforeOrAfter = direction !== 'after' ? $pos.index(-1) : $pos.indexAfter(-1); + const type = above.contentMatchAt(beforeOrAfter).defaultType; + + if (!above.canReplaceWith(beforeOrAfter, beforeOrAfter, type)) { + return false; + } + + if (dispatch) { + const insertPos = direction !== 'after' ? $pos.before() : $pos.after(); + const tr = state.tr.replaceWith(insertPos, insertPos, type.createAndFill()); + tr.setSelection(Selection.near(tr.doc.resolve(insertPos), 1)); + dispatch(tr.scrollIntoView()); + } + return true; + }; +} diff --git a/script/main.js b/script/main.js index 6c2f183e..585d6f98 100644 --- a/script/main.js +++ b/script/main.js @@ -8,11 +8,13 @@ import getKeymapPlugin from './plugins/Keymap/keymap'; import initializePublicAPI from './initializePublicAPI'; import MenuInitializer from './plugins/Menu/MenuInitializer'; import getNodeViews from './nodeviews'; +import { initializeButtons } from './custom/paragraphButtons'; initializePublicAPI(); window.Prosemirror.enableProsemirror = function enableProsemirror() { const schema = new Schema(getSpec()); + initializeButtons(); const mi = new MenuInitializer(schema); @@ -40,7 +42,9 @@ window.Prosemirror.enableProsemirror = function enableProsemirror() { }, nodeViews: getNodeViews(), }); - window.view = view; + window.Prosemirror.views = { + main: view, + }; jQuery(window).on('scroll.prosemirror_menu', () => { const $container = jQuery('#prosemirror__editor'); const $menuBar = $container.find('.menubar'); @@ -56,8 +60,8 @@ window.Prosemirror.enableProsemirror = function enableProsemirror() { }; window.Prosemirror.destroyProsemirror = function destroyProsemirror() { - if (window.view && typeof window.view.destroy === 'function') { - window.view.destroy(); + if (window.Prosemirror.views.main && typeof window.Prosemirror.views.main.destroy === 'function') { + window.Prosemirror.views.main.destroy(); } jQuery(window).off('scroll.prosemirror_menu'); }; diff --git a/script/nodeviews/Footnote/footnoteSchema.js b/script/nodeviews/Footnote/footnoteSchema.js index f0529cc9..bd98e210 100644 --- a/script/nodeviews/Footnote/footnoteSchema.js +++ b/script/nodeviews/Footnote/footnoteSchema.js @@ -1,7 +1,7 @@ import getSpec from '../../schema'; -export default function getFootnoteSpec() { - const baseSpec = getSpec(); +export default function getFootnoteSpec(viewID) { + const baseSpec = getSpec(viewID); let footnoteSchemaNodes = baseSpec.nodes.remove('footnote').remove('heading'); const doc = { ...footnoteSchemaNodes.get('doc') }; const { notoc: ommitted, nocache: ommitted2, ...newDocAttrs } = doc.attrs; diff --git a/script/nodeviews/FootnoteView.js b/script/nodeviews/FootnoteView.js index e2816acd..57a76044 100644 --- a/script/nodeviews/FootnoteView.js +++ b/script/nodeviews/FootnoteView.js @@ -45,8 +45,10 @@ class FootnoteView extends AbstractNodeView { appendTo: '.dokuwiki', close: this.dispatchOuter.bind(this), }); + const viewID = 'footnoteView'; + // And put a sub-ProseMirror into that - const footnoteSchema = new Schema(getFootnoteSpec()); + const footnoteSchema = new Schema(getFootnoteSpec(viewID)); const mi = new MenuInitializer(footnoteSchema); this.innerView = new EditorView(this.tooltip, { // You can use any node as an editor document @@ -72,6 +74,7 @@ class FootnoteView extends AbstractNodeView { }, nodeViews: getNodeViews(), }); + window.Prosemirror.views[viewID] = this.innerView; } close() { diff --git a/script/schema.js b/script/schema.js index a6c5f9e6..26a0c277 100644 --- a/script/schema.js +++ b/script/schema.js @@ -7,9 +7,17 @@ import { schema as schemaBasic } from 'prosemirror-schema-basic'; import { tableNodes } from 'prosemirror-tables'; import { bulletList, listItem, orderedList } from 'prosemirror-schema-list'; +import { getButtonSpec } from './custom/paragraphButtons'; -export default function getSpec() { +/** + * @param {string} viewID key that defines the view for this schema in window.Prosemirror.views + * + * @return {{nodes: OrderedMap, marks: OrderedMap }} + */ +export default function getSpec(viewID = 'main') { let { nodes, marks } = schemaBasic.spec; + const buttonParagraphBeforeSpec = getButtonSpec(viewID, 'before'); + const buttonParagraphAfterSpec = getButtonSpec(viewID, 'after'); const doc = nodes.get('doc'); doc.content = '(block | baseonly | container | protected_block | substitution_block)+'; @@ -89,7 +97,12 @@ export default function getSpec() { code: true, defining: true, toDOM(node) { - return ['pre', node.attrs, 0]; + return [ + 'div', + buttonParagraphBeforeSpec, + ['pre', node.attrs, 0], + buttonParagraphAfterSpec, + ]; }, });