From ca93044f34ea4dd385f58ed5ef7762e43330a465 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Sat, 21 Dec 2024 22:59:20 +0100 Subject: [PATCH] feat: keep selection highlighted when opening dialogue --- private/css/cms.balloon-toolbar.css | 8 +- private/css/cms.linkfield.css | 2 +- private/css/cms.tiptap.css | 11 ++- private/js/cms.linkfield.js | 10 ++- private/js/cms.tiptap.js | 21 +++--- private/js/tiptap_plugins/cms.dynlink.js | 3 +- .../js/tiptap_plugins/cms.formextension.js | 75 ++++++++++++++++++- .../js/tiptap_plugins/cms.tiptap.toolbar.js | 17 ++--- 8 files changed, 111 insertions(+), 36 deletions(-) diff --git a/private/css/cms.balloon-toolbar.css b/private/css/cms.balloon-toolbar.css index d1b5a3e3..c215260f 100644 --- a/private/css/cms.balloon-toolbar.css +++ b/private/css/cms.balloon-toolbar.css @@ -31,14 +31,8 @@ } } -@keyframes delayedHide { - to { +.cms-editor-inline-wrapper:not(:has(.cms-toolbar.show)) .cms-balloon { visibility: hidden; - } -} - -.cms-editor-inline-wrapper:not(:has(.ProseMirror-focused)) .cms-balloon { - animation: 0s linear 0.2s forwards delayedHide; } dialog.cms-form-dialog.cms-balloon-menu { diff --git a/private/css/cms.linkfield.css b/private/css/cms.linkfield.css index b576dcf9..d545d828 100644 --- a/private/css/cms.linkfield.css +++ b/private/css/cms.linkfield.css @@ -3,7 +3,7 @@ font-size: 0.8rem; position: relative; input[type="text"] { - padding-inline-end: 2em; + padding-inline-end: 2em !important; background: var(--dca-white) url('data:image/svg+xml;utf8,') no-repeat right center; &[dir=rtl] { background-position: left center; diff --git a/private/css/cms.tiptap.css b/private/css/cms.tiptap.css index 70c25c11..86b22895 100644 --- a/private/css/cms.tiptap.css +++ b/private/css/cms.tiptap.css @@ -45,12 +45,17 @@ a[href] { cursor: pointer; } + .fake-selection { + background-color: Highlight; + color: HighlightText; + } table { th, td { position: relative; } .selectedCell { - background: color-mix(in srgb, var(--dca-primary) 20%, transparent); + color: HighlightText; + background: Highlight; } .column-resize-handle { top: 0; @@ -58,8 +63,8 @@ right: -1px; width: 2px; position: absolute; - background: var(--dca-primary); - box-shadow: 0 0 2px var(--dca-primary); + background: Highlight; + box-shadow: 0 0 2px Highlight; } } & cms-plugin { diff --git a/private/js/cms.linkfield.js b/private/js/cms.linkfield.js index e84286c1..8ea65fc1 100644 --- a/private/js/cms.linkfield.js +++ b/private/js/cms.linkfield.js @@ -5,6 +5,8 @@ class LinkField { constructor(element, options) { + const hasFocus = element.contains(document.activeElement); + this.options = options; this.urlElement = element; this.form = element.closest("form"); @@ -12,13 +14,13 @@ class LinkField { if (this.selectElement) { this.urlElement.setAttribute('type', 'hidden'); // Two input types? this.selectElement.setAttribute('type', 'hidden'); // Make hidden and add common input - this.createInput(); + this.createInput(hasFocus); this.registerEvents(); } this.populateField(); } - createInput() { + createInput(hasFocus = false) { this.inputElement = document.createElement('input'); this.inputElement.setAttribute('type', 'text'); this.inputElement.setAttribute('autocomplete', 'off'); @@ -40,7 +42,9 @@ class LinkField { } this.wrapper.appendChild(this.inputElement); this.wrapper.appendChild(this.dropdown); - + if (hasFocus) { + this.inputElement.focus(); + } } populateField() { diff --git a/private/js/cms.tiptap.js b/private/js/cms.tiptap.js index 215c8a49..dc9cb822 100644 --- a/private/js/cms.tiptap.js +++ b/private/js/cms.tiptap.js @@ -14,22 +14,23 @@ import Table from '@tiptap/extension-table'; import TableCell from '@tiptap/extension-table-cell'; import TableHeader from '@tiptap/extension-table-header'; import TableRow from '@tiptap/extension-table-row'; -import { TextAlign, TextAlignOptions } from '@tiptap/extension-text-align'; -import { CmsPluginNode, CmsBlockPluginNode } from './tiptap_plugins/cms.plugin'; +import {TextAlign, TextAlignOptions} from '@tiptap/extension-text-align'; +import {CmsPluginNode, CmsBlockPluginNode} from './tiptap_plugins/cms.plugin'; import TiptapToolbar from "./tiptap_plugins/cms.tiptap.toolbar"; import {StarterKit} from "@tiptap/starter-kit"; -import { InlineColors, Small, Var, Kbd, Samp } from "./tiptap_plugins/cms.styles"; +import {InlineColors, Small, Var, Kbd, Samp} from "./tiptap_plugins/cms.styles"; import CmsBalloonToolbar from "./tiptap_plugins/cms.balloon-toolbar"; import FormExtension from "./tiptap_plugins/cms.formextension"; -import { formToHtml, populateForm } from './cms.dialog'; +import {formToHtml, populateForm} from './cms.dialog'; import LinkField from './cms.linkfield'; import '../css/cms.tiptap.css'; import '../css/cms.linkfield.css'; + class CMSTipTapPlugin { defaultOptions() { return { @@ -62,12 +63,12 @@ class CMSTipTapPlugin { ], toolbar_HTMLField: [ ['Paragraph', '-', 'Heading1', 'Heading2', 'Heading3', 'Heading4', 'Heading5'], '|', - ['Bold', 'Italic', 'Underline', 'Strike', '-', 'Subscript', 'Superscript', '-', 'RemoveFormat'] + ['Bold', 'Italic', 'Underline', 'Strike', '-', 'Subscript', 'Superscript', '-', 'RemoveFormat'], ['Undo', 'Redo'], ], toolbar_CMS: [ ['Paragraph', '-', 'Heading1', 'Heading2', 'Heading3', 'Heading4', 'Heading5'], '|', - ['Bold', 'Italic', 'Underline', 'Strike', '-', 'Subscript', 'Superscript', '-', 'RemoveFormat'] + ['Bold', 'Italic', 'Underline', 'Strike', '-', 'Subscript', 'Superscript', '-', 'RemoveFormat'], ['Undo', 'Redo'], ], }; @@ -226,7 +227,7 @@ class CMSTipTapPlugin { } _createBlockToolbar(el, editor, options) { - const toolbar = this._populateToolbar(editor,options.toolbar || this.options.toolbar_HTMLField, 'block'); + const toolbar = this._populateToolbar(editor, options.toolbar || this.options.toolbar_HTMLField, 'block'); const ballonToolbar = new CmsBalloonToolbar(editor, toolbar, (event) => this._handleToolbarClick(event, editor), (el) => this._updateToolbar(editor, el)); @@ -352,17 +353,19 @@ class CMSTipTapPlugin { } - // Blur editor event + // Blur editor event _blurEditor(editor, event) { // Let the editor process clicks on the toolbar first // This hopefully prevents race conditions setTimeout(() => { // Allow toolbar and other editor widgets to process the click first // They need to refocus the editor to avoid a save - if (!editor.options.element.contains(document.activeElement)) { + if(!editor.options.el.contains(document.activeElement)) { // hide the toolbar editor.options.element.querySelectorAll('[role="menubar"], [role="button"]') .forEach((el) => el.classList.remove('show')); + } + if (!editor.options.element.contains(document.activeElement)) { // save the content (is no-op for non-inline calls) editor.options.save_callback(); } diff --git a/private/js/tiptap_plugins/cms.dynlink.js b/private/js/tiptap_plugins/cms.dynlink.js index c4cd7fba..f946352c 100644 --- a/private/js/tiptap_plugins/cms.dynlink.js +++ b/private/js/tiptap_plugins/cms.dynlink.js @@ -1,6 +1,7 @@ /* eslint-env es11 */ /* jshint esversion: 11 */ /* global document, window, console */ +'use strict'; import Link from '@tiptap/extension-link'; @@ -30,12 +31,12 @@ const CmsDynLink = Link.extend({ event.preventDefault(); setTimeout(() => { if (this.editor.isActive('link')) { + this.editor.commands.extendMarkRange('link'); this.editor.commands.openCmsForm('Link'); } }, 0); } }).bind(this); // hacky: move the eventHandler to the Mark object (this) to be able to remove it later - console.log("Event", editor.view.dom); editor.view.dom.addEventListener('click', this); }, diff --git a/private/js/tiptap_plugins/cms.formextension.js b/private/js/tiptap_plugins/cms.formextension.js index 7e9595e0..029b55bb 100644 --- a/private/js/tiptap_plugins/cms.formextension.js +++ b/private/js/tiptap_plugins/cms.formextension.js @@ -1,12 +1,69 @@ /* eslint-env es6 */ /* jshint esversion: 6 */ /* global document, window, console */ +'use strict'; import {CmsForm, formToHtml, populateForm} from "../cms.dialog"; import {Extension} from "@tiptap/core"; import TiptapToolbar from "./cms.tiptap.toolbar"; import LinkField from "../cms.linkfield"; +import {Decoration, DecorationSet} from '@tiptap/pm/view'; +import {Plugin} from '@tiptap/pm/state'; + + +// ProseMirror plugin to handle temporary decorations +const _fakeSelectionPlugin = new Plugin({ + state: { + init(_, {doc}) { + return DecorationSet.empty; + }, + apply(tr, decorationSet) { + // Remove decoration on any transaction unless explicitly preserved + if (tr.getMeta("fake-selection") === "add") { + const {from, to} = tr.selection; + const decoration = Decoration.inline(from, to, {class: "fake-selection"}); + return decorationSet.add(tr.doc, [decoration]); + } else if (tr.getMeta("fake-selection") === "remove") { + const decorations = decorationSet.find().filter( + (decoration) => decoration.spec?.class === "fake-selection" + ); + return DecorationSet.create(tr.doc, decorations); + } + return decorationSet.map(tr.mapping, tr.doc); + }, + }, + props: { + decorations(state) { + return this.getState(state); + }, + }, +}); + + +const fakeSelectionPlugin = Extension.create({ + addProseMirrorPlugins() { + return [_fakeSelectionPlugin]; + }, +}); + + +function addFakeSelection(view) { + const {state, dispatch} = view; + const tr = state.tr; + // Add meta to trigger the plugin + tr.setMeta("fake-selection", "add"); + dispatch(tr); +} + +function clearFakeSelection(view) { + const {state, dispatch} = view; + const tr = state.tr; + // Add meta to trigger the plugin + tr.setMeta("fake-selection", "remove"); + dispatch(tr); +} + const FormExtension = Extension.create({ @@ -16,6 +73,7 @@ const FormExtension = Extension.create({ return { openCmsForm: (action, target) => ({editor, commands}) => { let options; + addFakeSelection(editor.view); if (target) { const rect = target.getBoundingClientRect(); options = { @@ -40,8 +98,11 @@ const FormExtension = Extension.create({ } const dialog = new CmsForm( editor.options.element, - data => TiptapToolbar[action].formAction(editor, data), - () => editor.commands.focus() + data => { + TiptapToolbar[action].formAction(editor, data); + edior.commands.closeCmsForm(); + }, + () => editor.commands.closeCmsForm() ); const formRepresentation = window.cms_editor_plugin._getRepresentation(action); const formElement = dialog.formDialog(formToHtml(formRepresentation.form), options); @@ -57,9 +118,17 @@ const FormExtension = Extension.create({ url: editor.options.settings.url_endpoint || '', }); }, this); + }, + + closeCmsForm: () => ({editor}) => { + clearFakeSelection(editor.view); + editor.commands.focus(); } }; - } + }, + addProseMirrorPlugins() { + return [_fakeSelectionPlugin]; + }, }); export default FormExtension; diff --git a/private/js/tiptap_plugins/cms.tiptap.toolbar.js b/private/js/tiptap_plugins/cms.tiptap.toolbar.js index 9078183d..06c5ad24 100644 --- a/private/js/tiptap_plugins/cms.tiptap.toolbar.js +++ b/private/js/tiptap_plugins/cms.tiptap.toolbar.js @@ -117,7 +117,13 @@ const TiptapToolbar = { type: 'block', }, Link: { - action: (editor) => editor.commands.openCmsForm('Link'), + action: (editor) => { + if (editor.isActive('link')) { + // If the user is currently editing a link, update the whole link + editor.commands.extendMarkRange('link'); + } + editor.commands.openCmsForm('Link'); + }, formAction: (editor, data) => { if (data) { const link = { @@ -125,14 +131,7 @@ const TiptapToolbar = { 'data-cms-href': data.get('href_select') || null, 'target': data.get('target') || null, }; - if (editor.isActive('link')) { - // If the user is currently editing a link, update the whole link - editor.chain().focus().extendMarkRange('link').setLink(link).run(); - } else { - editor.chain().focus().setLink(link).run(); - } - } else { - editor.focus(); + editor.commands.setLink(link); } }, enabled: (editor) => editor.can().setLink({href: '#'}),