diff --git a/README.md b/README.md
index 86bc590..3d0c5e8 100644
--- a/README.md
+++ b/README.md
@@ -338,7 +338,13 @@ Additionally your frontend can
- determine which types of text format (node) appear on the quanta toolbar when editing rich text, including adding custom formats ([TODO](https://github.com/collective/volto-hydra/issues/109))
- add a callback of ```onBlockFieldChange``` to rerender just the editable fields more quickly while editing (TODO)
-Note Hydra.js knows about the schema of your blocks and based on the field name will determine what what to make your html editable.
+Note Hydra.js knows about the schema of your blocks and based on the field name will determine what to make your html editable.
+
+IMPORTANT: Currently, Hydra.js supports BOLD, ITALIC & STRIKETHROUGH formats on slate blocks and following are the conditions when it breaks slate:
+- if you select the whole text and change its formats your frontend might throw slate error saying `Cannot get the leaf node at path [0,0] because it refers to a non-leaf node:` but it is due to proper syncing of json b/w hydrajs & adminUI.
+- At the endline if you press format button then it will change the state (active/inactive) but frontend might throw slate error/warning that `Error: Cannot find a descendant at path [0,4,0] in node:`
+- pressing ENTER is not implemented so, pressing it will have abnormal changes & error ([TODO](https://github.com/collective/volto-hydra/issues/33))
+These will not break the codebase completely as deserializer at the adminUI cleans up the html and make a proper
#### Inline media uploading ([TODO](https://github.com/collective/volto-hydra/issues/36))
diff --git a/examples/hydra-nextjs/src/components/Menu/Menu.js b/examples/hydra-nextjs/src/components/Menu/Menu.js
index 67181bc..177d0b5 100644
--- a/examples/hydra-nextjs/src/components/Menu/Menu.js
+++ b/examples/hydra-nextjs/src/components/Menu/Menu.js
@@ -1,7 +1,7 @@
'use client';
import React, { useEffect, useState } from 'react';
import { Menu as SemanticMenu } from 'semantic-ui-react';
-import { getTokenFromCookie } from '#utils/hydra';
+import { getTokenFromCookie } from '@volto-hydra/hydra-js';
import Link from 'next/link';
import RecursiveMenuItem from '@/components/RecursiveMenuItem';
import { fetchContent } from '#utils/api';
diff --git a/examples/hydra-nextjs/src/components/RecursiveMenuItem/RecursiveMenuItem.js b/examples/hydra-nextjs/src/components/RecursiveMenuItem/RecursiveMenuItem.js
index a0466bb..8719415 100644
--- a/examples/hydra-nextjs/src/components/RecursiveMenuItem/RecursiveMenuItem.js
+++ b/examples/hydra-nextjs/src/components/RecursiveMenuItem/RecursiveMenuItem.js
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
import { Menu as SemanticMenu } from 'semantic-ui-react';
import Link from 'next/link';
import HoverableDropdown from '@/components/HoverableDropdown';
-import { getTokenFromCookie } from '#utils/hydra';
+import { getTokenFromCookie } from '@volto-hydra/hydra-js';
import { fetchContent } from '#utils/api';
import extractEndpoints from '#utils/extractEndpoints';
diff --git a/packages/hydra-js/hydra.js b/packages/hydra-js/hydra.js
index 0583639..4520cc0 100644
--- a/packages/hydra-js/hydra.js
+++ b/packages/hydra-js/hydra.js
@@ -1,4 +1,4 @@
-/** Bridge class creating two-way link between the Hydra and the frontend **/
+/** Bridge class creating two-way link between the Hydra and the frontend */
class Bridge {
/**
*
@@ -23,6 +23,9 @@ class Bridge {
this.formData = null;
this.blockTextMutationObserver = null;
this.selectedBlockUid = null;
+ this.handleBlockFocusIn = null;
+ this.handleBlockFocusOut = null;
+ this.isInlineEditing = false;
this.init(options);
}
@@ -101,6 +104,10 @@ class Bridge {
} else {
throw new Error('No form data has been sent from the adminUI');
}
+ } else if (event.data.type === 'TOGGLE_MARK_DONE') {
+ console.log('toggle mark data rec');
+ this.formData = JSON.parse(JSON.stringify(event.data.data));
+ callback(event.data.data);
}
}
};
@@ -133,61 +140,21 @@ class Bridge {
document.removeEventListener('click', this.blockClickHandler);
document.addEventListener('click', this.blockClickHandler);
}
- /**
- * Method to add border, ADD button and Quanta toolbar to the selected block
- * @param {Element} blockElement - Block element with the data-block-uid attribute
- */
- selectBlock(blockElement) {
- // Helper function to handle each element
- const handleElement = (element) => {
- const editableField = element.getAttribute('data-editable-field');
- if (editableField === 'value') {
- this.makeBlockContentEditable(element);
- } else if (editableField !== null) {
- element.setAttribute('contenteditable', 'true');
- }
- };
-
- // Function to recursively handle all children
- const handleElementAndChildren = (element) => {
- handleElement(element);
- Array.from(element.children).forEach((child) =>
- handleElementAndChildren(child),
- );
- };
-
- // Remove border and button from the previously selected block
- if (this.currentlySelectedBlock) {
- this.deselectBlock(this.currentlySelectedBlock);
- }
- const blockUid = blockElement.getAttribute('data-block-uid');
- this.selectedBlockUid = blockUid;
-
- // Handle the selected block and its children for contenteditable
- handleElementAndChildren(blockElement);
- // Only when the block is a slate block, add nodeIds to the block's data
- this.observeBlockTextChanges(blockElement);
- // if the block is a slate block, add nodeIds to the block's data
- if (this.formData && this.formData.blocks[blockUid]['@type'] === 'slate') {
- this.formData.blocks[blockUid] = this.addNodeIds(
- this.formData.blocks[blockUid],
- );
- this.setDataCallback(this.formData);
+ createQuantaToolbar(blockUid, show = { formatBtns: true }) {
+ // Check if the toolbar already exists
+ if (this.quantaToolbar) {
+ return;
}
- // Add focus out event listener
- blockElement.addEventListener(
- 'focusout',
- this.handleBlockFocusOut.bind(this),
- );
+ // Create the quantaToolbar
+ this.quantaToolbar = document.createElement('div');
+ this.quantaToolbar.className = 'volto-hydra-quantaToolbar';
- // Set the currently selected block
- this.currentlySelectedBlock = blockElement;
- // Add border to the currently selected block
- this.currentlySelectedBlock.classList.add('volto-hydra--outline');
+ // Prevent click event propagation for the quantaToolbar
+ this.quantaToolbar.addEventListener('click', (e) => e.stopPropagation());
- // Create and append the Add button
+ // Create the Add button
this.addButton = document.createElement('button');
this.addButton.className = 'volto-hydra-add-button';
this.addButton.innerHTML = addSVG;
@@ -200,23 +167,85 @@ class Bridge {
};
this.currentlySelectedBlock.appendChild(this.addButton);
- // Create the quantaToolbar
- this.quantaToolbar = document.createElement('div');
- this.quantaToolbar.className = 'volto-hydra-quantaToolbar';
-
- // Prevent event propagation for the quantaToolbar
- this.quantaToolbar.addEventListener('click', (e) => e.stopPropagation());
-
// Create the drag button
const dragButton = document.createElement('button');
dragButton.className = 'volto-hydra-drag-button';
- dragButton.innerHTML = dragSVG; // Use your drag SVG here
- dragButton.disabled = true; // Disable drag button for now
+ dragButton.innerHTML = dragSVG;
+ dragButton.disabled = true;
+
+ let boldButton = null;
+ let italicButton = null;
+ let delButton = null;
+
+ if (show.formatBtns) {
+ // Create the bold button
+ boldButton = document.createElement('button');
+ boldButton.className = `volto-hydra-format-button ${
+ show.formatBtns ? 'show' : ''
+ }`;
+ boldButton.innerHTML = boldSVG;
+ boldButton.addEventListener('click', () => {
+ this.formatSelectedText('bold');
+ });
+
+ // Create the italic button
+ italicButton = document.createElement('button');
+ italicButton.className = `volto-hydra-format-button ${
+ show.formatBtns ? 'show' : ''
+ }`;
+ italicButton.innerHTML = italicSVG;
+ italicButton.addEventListener('click', () => {
+ this.formatSelectedText('italic');
+ });
+
+ // Create the del button
+ delButton = document.createElement('button');
+ delButton.className = `volto-hydra-format-button ${
+ show.formatBtns ? 'show' : ''
+ }`;
+ delButton.innerHTML = delSVG;
+ delButton.addEventListener('click', () => {
+ this.formatSelectedText('del');
+ });
+
+ // Function to handle the text selection and show/hide the bold button
+ const handleSelectionChange = () => {
+ const selection = window.getSelection();
+ const range = selection.getRangeAt(0);
+
+ // Append the bold button only if text is selected and the block has the data-editable-field="value" attribute
+
+ const formats = this.isFormatted(range);
+ boldButton.classList.toggle(
+ 'active',
+ formats.bold.enclosing || formats.bold.present,
+ );
+ italicButton.classList.toggle(
+ 'active',
+ formats.italic.enclosing || formats.italic.present,
+ );
+ delButton.classList.toggle(
+ 'active',
+ formats.del.enclosing || formats.del.present,
+ );
+ };
+
+ // Add event listener to handle text selection within the block
+ this.handleMouseUp = (e) => {
+ if (e.target.closest('[data-editable-field="value"]')) {
+ handleSelectionChange();
+ }
+ };
+ this.currentlySelectedBlock.addEventListener(
+ 'mouseup',
+ this.handleMouseUp,
+ );
+ }
// Create the three-dot menu button
const menuButton = document.createElement('button');
menuButton.className = 'volto-hydra-menu-button';
- menuButton.innerHTML = threeDotsSVG; // Use your three dots SVG here
+ menuButton.innerHTML = threeDotsSVG;
// Create the dropdown menu
const dropdownMenu = document.createElement('div');
@@ -256,11 +285,103 @@ class Bridge {
// Append elements to the quantaToolbar
this.quantaToolbar.appendChild(dragButton);
+ if (show.formatBtns) {
+ this.quantaToolbar.appendChild(boldButton);
+ this.quantaToolbar.appendChild(italicButton);
+ this.quantaToolbar.appendChild(delButton);
+ }
this.quantaToolbar.appendChild(menuButton);
this.quantaToolbar.appendChild(dropdownMenu);
// Append the quantaToolbar to the currently selected block
this.currentlySelectedBlock.appendChild(this.quantaToolbar);
+ }
+
+ /**
+ * Method to add border, ADD button and Quanta toolbar to the selected block
+ * @param {Element} blockElement - Block element with the data-block-uid attribute
+ */
+ selectBlock(blockElement) {
+ // Remove border and button from the previously selected block
+ if (this.currentlySelectedBlock) {
+ this.deselectBlock(this.currentlySelectedBlock, blockElement);
+ }
+ if (
+ this.currentlySelectedBlock === null ||
+ this.currentlySelectedBlock !== blockElement
+ ) {
+ this.handleBlockFocusOut = (e) => {
+ // console.log("focus out");
+ window.parent.postMessage(
+ { type: 'INLINE_EDIT_EXIT' },
+ this.adminOrigin,
+ );
+ };
+ this.handleBlockFocusIn = (e) => {
+ // console.log("focus in");
+ window.parent.postMessage(
+ {
+ type: 'INLINE_EDIT_ENTER',
+ },
+ this.adminOrigin,
+ );
+ this.isInlineEditing = true;
+ };
+ // Add focus in event listener
+ blockElement.addEventListener(
+ 'focusout',
+ this.handleBlockFocusOut.bind(this),
+ );
+
+ blockElement.addEventListener(
+ 'focusin',
+ this.handleBlockFocusIn.bind(this),
+ );
+ }
+ // Helper function to handle each element
+ const handleElement = (element) => {
+ const editableField = element.getAttribute('data-editable-field');
+ if (editableField === 'value') {
+ this.makeBlockContentEditable(element);
+ } else if (editableField !== null) {
+ element.setAttribute('contenteditable', 'true');
+ }
+ };
+
+ // Function to recursively handle all children
+ const handleElementAndChildren = (element) => {
+ handleElement(element);
+ Array.from(element.children).forEach((child) =>
+ handleElementAndChildren(child),
+ );
+ };
+
+ const blockUid = blockElement.getAttribute('data-block-uid');
+ this.selectedBlockUid = blockUid;
+
+ // Handle the selected block and its children for contenteditable
+ handleElementAndChildren(blockElement);
+ let show = { formatBtns: false };
+ this.observeBlockTextChanges(blockElement);
+ // // if the block is a slate block, add nodeIds to the block's data
+ if (this.formData && this.formData.blocks[blockUid]['@type'] === 'slate') {
+ show.formatBtns = true;
+ this.formData.blocks[blockUid] = this.addNodeIds(
+ this.formData.blocks[blockUid],
+ );
+ this.setDataCallback(this.formData);
+ // window.parent.postMessage(
+ // { type: "ADD_NODEIDS", data: this.formData },
+ // this.adminOrigin
+ // );
+ }
+
+ // Set the currently selected block
+ this.currentlySelectedBlock = blockElement;
+ // Add border to the currently selected block
+ this.currentlySelectedBlock.classList.add('volto-hydra--outline');
+
+ if (this.formData) this.createQuantaToolbar(blockUid, show);
if (!this.clickOnBtn) {
window.parent.postMessage(
@@ -377,38 +498,44 @@ class Bridge {
* Reset the block's listeners, mutation observer and remove the nodeIds from the block's data
* @param {Element} blockElement Selected block element
*/
- deselectBlock(blockElement) {
- this.currentlySelectedBlock.classList.remove('volto-hydra--outline');
- if (this.addButton) {
- this.addButton.remove();
- this.addButton = null;
- }
- if (this.deleteButton) {
- this.deleteButton.remove();
- this.deleteButton = null;
- }
- if (this.quantaToolbar) {
- this.quantaToolbar.remove();
- this.quantaToolbar = null;
- }
- const blockUid = blockElement.getAttribute('data-block-uid');
- if (this.selectedBlockUid !== null && this.selectedBlockUid !== blockUid) {
+ deselectBlock(prevBlockElement, currBlockElement) {
+ const currBlockUid = currBlockElement.getAttribute('data-block-uid');
+ if (
+ this.selectedBlockUid !== null &&
+ this.selectedBlockUid !== currBlockUid
+ ) {
+ this.currentlySelectedBlock.classList.remove('volto-hydra--outline');
+ if (this.addButton) {
+ this.addButton.remove();
+ this.addButton = null;
+ }
+ if (this.deleteButton) {
+ this.deleteButton.remove();
+ this.deleteButton = null;
+ }
+ if (this.quantaToolbar) {
+ this.quantaToolbar.remove();
+ this.quantaToolbar = null;
+ }
// Remove contenteditable attribute
- blockElement.removeAttribute('contenteditable');
- const childNodes = blockElement.querySelectorAll('[data-hydra-node]');
+ prevBlockElement.removeAttribute('contenteditable');
+ const childNodes = prevBlockElement.querySelectorAll('[data-hydra-node]');
childNodes.forEach((node) => {
node.removeAttribute('contenteditable');
});
// Clean up JSON structure
- this.resetJsonNodeIds(this.blocksJson);
+ // if (this.formData.blocks[this.selectedBlockUid]["@type"] === "slate") this.resetJsonNodeIds(this.formData.blocks[this.selectedBlockUid]);
// Remove focus out event listener
- blockElement.removeEventListener(
+ prevBlockElement.removeEventListener(
'focusout',
- this.handleBlockFocusOut.bind(this),
+ this.handleBlockFocusOut,
);
+ // Remove focus in event listener
+ prevBlockElement.removeEventListener('focusin', this.handleBlockFocusIn);
}
+ document.removeEventListener('mouseup', this.handleMouseUp);
// Disconnect the mutation observer
if (this.blockTextMutationObserver) {
this.blockTextMutationObserver.disconnect();
@@ -442,38 +569,55 @@ class Bridge {
observeBlockTextChanges(blockElement) {
this.blockTextMutationObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
- if (
- mutation.type === 'characterData' ||
- mutation.type === 'childList'
- ) {
- let targetElement = null;
-
- if (mutation.type === 'characterData') {
- targetElement =
- mutation.target?.parentElement.closest('[data-hydra-node]');
- } else {
- targetElement = mutation.target.closest('[data-hydra-node]');
- }
+ if (mutation.type === 'characterData') {
+ const targetElement =
+ mutation.target?.parentElement.closest('[data-hydra-node]');
- if (targetElement) {
- this.handleTextChange(targetElement);
+ if (targetElement && this.isInlineEditing) {
+ this.handleTextChangeOnSlate(targetElement);
+ } else if (this.isInlineEditing) {
+ const targetElement = mutation.target?.parentElement.closest(
+ '[data-editable-field]',
+ );
+ if (targetElement) {
+ this.handleTextChange(targetElement);
+ }
}
}
});
});
this.blockTextMutationObserver.observe(blockElement, {
- childList: true,
subtree: true,
characterData: true,
});
}
/**
- * Handle the text change in the slate block element
+ * Handle the text change in the block element with attr data-editable-field
* @param {Element} target
*/
handleTextChange(target) {
+ const blockUid = target
+ .closest('[data-block-uid]')
+ .getAttribute('data-block-uid');
+ const editableField = target.getAttribute('data-editable-field');
+ if (editableField)
+ this.formData.blocks[blockUid][editableField] = target.innerText;
+ console.log('editableField', this.formData.blocks[blockUid][editableField]);
+ if (this.formData.blocks[blockUid]['@type'] !== 'slate') {
+ window.parent.postMessage(
+ { type: 'INLINE_EDIT_DATA', data: this.formData },
+ this.adminOrigin,
+ );
+ }
+ }
+
+ /**
+ * Handle the text change in the slate block element
+ * @param {Element} target
+ */
+ handleTextChangeOnSlate(target) {
const closestNode = target.closest('[data-hydra-node]');
if (closestNode) {
const nodeId = closestNode.getAttribute('data-hydra-node');
@@ -515,8 +659,279 @@ class Bridge {
return json;
}
- handleBlockFocusOut(e) {
- window.parent.postMessage({ type: 'INLINE_EDIT_EXIT' }, this.adminOrigin);
+ findParentWithAttribute(node, attribute) {
+ while (node && node.nodeType === Node.ELEMENT_NODE) {
+ if (node.hasAttribute(attribute)) {
+ return node;
+ }
+ node = node.parentElement;
+ }
+ return null;
+ }
+
+ getSelectionHTML(range) {
+ const div = document.createElement('div');
+ div.appendChild(range.cloneContents());
+ return div.innerHTML;
+ }
+ isFormatted(range) {
+ const formats = {
+ bold: { present: false, enclosing: false },
+ italic: { present: false, enclosing: false },
+ del: { present: false, enclosing: false },
+ };
+
+ // Check if the selection is collapsed (empty)
+ // if (range.collapsed) return formats;
+
+ // Get the common ancestor container of the selection
+ let container = range.commonAncestorContainer;
+
+ // Traverse upwards until we find the editable parent or the root
+ while (
+ container &&
+ container !== document &&
+ !(container.dataset && container.dataset.editableField === 'value')
+ ) {
+ // Check if the container itself has any of the formatting
+ if (container.nodeName === 'STRONG' || container.nodeName === 'B') {
+ if (
+ container.contains(range.startContainer) &&
+ container.contains(range.endContainer)
+ ) {
+ formats.bold.enclosing = true;
+ formats.bold.present = true;
+ }
+ }
+ if (container.nodeName === 'EM' || container.nodeName === 'I') {
+ if (
+ container.contains(range.startContainer) &&
+ container.contains(range.endContainer)
+ ) {
+ formats.italic.enclosing = true;
+ formats.italic.present = true;
+ }
+ }
+ if (container.nodeName === 'DEL') {
+ if (
+ container.contains(range.startContainer) &&
+ container.contains(range.endContainer)
+ ) {
+ formats.del.enclosing = true;
+ formats.del.present = true;
+ }
+ }
+
+ container = container.parentNode;
+ }
+
+ // Check for formatting within the selection
+ const selectionHTML = this.getSelectionHTML(range).toString();
+ if (selectionHTML.includes('') || selectionHTML.includes('')) {
+ formats.bold.present = true;
+ }
+ if (selectionHTML.includes('') || selectionHTML.includes('')) {
+ formats.italic.present = true;
+ }
+ if (selectionHTML.includes('')) {
+ formats.del.present = true;
+ }
+
+ return formats;
+ }
+
+ // Helper function to get the next node in the selection
+ nextNode(node) {
+ if (!node) return null; // Handle the case where node is null
+
+ if (node.firstChild) return node.firstChild;
+
+ while (node) {
+ if (node.nextSibling) return node.nextSibling;
+ node = node.parentNode;
+ }
+
+ return null; // Reached the end, return null
+ }
+
+ formatSelectedText(format) {
+ this.isInlineEditing = false;
+ const selection = window.getSelection();
+ if (!selection.rangeCount) return;
+
+ const range = selection.getRangeAt(0);
+ const currentFormats = this.isFormatted(range);
+
+ if (currentFormats[format].present) {
+ this.unwrapFormatting(range, format);
+ } else {
+ // Handle selections that include non-Text nodes
+ const fragment = range.extractContents(); // Extract the selected content
+ const newNode = document.createElement(
+ format === 'bold'
+ ? 'strong'
+ : format === 'italic'
+ ? 'em'
+ : format === 'del'
+ ? 'del'
+ : 'span',
+ );
+ newNode.appendChild(fragment); // Append the extracted content to the new node
+ range.insertNode(newNode); // Insert the new node back into the document
+ }
+ this.sendFormattedHTMLToAdminUI(selection);
+ }
+
+ // Helper function to unwrap formatting while preserving other formatting
+ unwrapFormatting(range, format) {
+ const formattingElements = {
+ bold: ['STRONG', 'B'],
+ italic: ['EM', 'I'],
+ del: ['DEL'],
+ };
+ const selection = window.getSelection();
+ // Check if the selection is entirely within a formatting element of the specified type
+ let container = range.commonAncestorContainer;
+ while (
+ container &&
+ container !== document &&
+ !(container.dataset && container.dataset.editableField === 'value')
+ ) {
+ if (formattingElements[format].includes(container.nodeName)) {
+ // Check if the entire content of the formatting element is selected
+ const isEntireContentSelected =
+ range.startOffset === 0 &&
+ range.endOffset === container.textContent.length;
+
+ if (isEntireContentSelected || selection.isCollapsed) {
+ // Unwrap the entire element
+ this.unwrapElement(container);
+ } else {
+ // Unwrap only the selected portion
+ this.unwrapSelectedPortion(
+ container,
+ range,
+ format,
+ formattingElements,
+ );
+ }
+ return; // No need to check further
+ }
+ container = container.parentNode;
+ }
+
+ // If the selection is not entirely within a formatting element, remove all occurrences of the format within the selection
+ let node = range.startContainer;
+ while (node && node !== range.endContainer) {
+ if (
+ node.nodeType === Node.ELEMENT_NODE &&
+ formattingElements[format].includes(node.nodeName)
+ ) {
+ this.unwrapElement(node);
+ } else if (
+ node.nodeType === Node.TEXT_NODE &&
+ node.parentNode &&
+ formattingElements[format].includes(node.parentNode.nodeName)
+ ) {
+ // Handle the case where the text node itself is within the formatting element
+ this.unwrapElement(node.parentNode);
+ }
+ node = this.nextNode(node);
+ }
+ }
+
+ // Helper function to unwrap the selected portion within a formatting element
+ unwrapSelectedPortion(element, range, format, formattingElements) {
+ const formattingTag = formattingElements[format][0];
+
+ // Check if selection starts at the beginning of the formatting element
+ const selectionStartsAtBeginning = range.startOffset === 0;
+
+ // Check if selection ends at the end of the formatting element
+ const selectionEndsAtEnd = range.endOffset === element.textContent.length;
+
+ // Extract the contents before the selection (only if not at the beginning)
+ let beforeFragment = null;
+ if (!selectionStartsAtBeginning) {
+ const beforeRange = document.createRange();
+ beforeRange.setStart(element, 0);
+ beforeRange.setEnd(range.startContainer, range.startOffset);
+ beforeFragment = beforeRange.extractContents();
+ }
+
+ // Extract the selected contents
+ const selectionFragment = range.extractContents();
+
+ // Extract the contents after the selection (only if not at the end)
+ let afterFragment = null;
+ if (!selectionEndsAtEnd) {
+ const afterRange = document.createRange();
+ afterRange.setStart(range.endContainer, range.endOffset);
+ afterRange.setEnd(element, element.childNodes.length);
+ afterFragment = afterRange.extractContents();
+ }
+
+ // Create new elements to wrap the before and after fragments, keeping the original formatting (only if fragments exist)
+ const beforeWrapper = beforeFragment
+ ? document.createElement(formattingTag)
+ : null;
+ if (beforeWrapper) {
+ beforeWrapper.appendChild(beforeFragment);
+ }
+ const afterWrapper = afterFragment
+ ? document.createElement(formattingTag)
+ : null;
+ if (afterWrapper) {
+ afterWrapper.appendChild(afterFragment);
+ }
+
+ // Replace the original element with the unwrapped selection and the formatted before/after parts
+ const parent = element.parentNode;
+ if (beforeWrapper) {
+ parent.insertBefore(beforeWrapper, element);
+ }
+ parent.insertBefore(selectionFragment, element);
+ if (afterWrapper) {
+ parent.insertBefore(afterWrapper, element);
+ }
+ parent.removeChild(element);
+ }
+
+ // Helper function to unwrap a single formatting element
+ unwrapElement(element) {
+ const parent = element.parentNode;
+ while (element.firstChild) {
+ parent.insertBefore(element.firstChild, element);
+ }
+ parent.removeChild(element);
+ }
+ sendFormattedHTMLToAdminUI(selection) {
+ if (!selection.rangeCount) return; // No selection
+
+ const range = selection.getRangeAt(0);
+ const commonAncestor = range.commonAncestorContainer;
+
+ const editableParent = this.findEditableParent(commonAncestor);
+ if (!editableParent) return; // Couldn't find the editable parent
+
+ const htmlString = editableParent.outerHTML;
+
+ window.parent.postMessage(
+ {
+ type: 'TOGGLE_MARK',
+ html: htmlString,
+ },
+ this.adminOrigin,
+ );
+ }
+ findEditableParent(node) {
+ if (!node || node === document) return null; // Reached the top without finding
+
+ if (node.dataset && node.dataset.editableField === 'value') {
+ return node;
+ }
+
+ return this.findEditableParent(node.parentNode);
}
injectCSS() {
const style = document.createElement('style');
@@ -575,17 +990,30 @@ class Bridge {
top: -45px;
left: 0;
box-sizing: border-box;
- width: 70px;
+ width: fit-content;
height: 40px;
}
.volto-hydra-drag-button,
- .volto-hydra-menu-button {
+ .volto-hydra-menu-button,
+ .volto-hydra-format-button {
background: none;
border: none;
cursor: pointer;
padding: 0.5em;
margin: 0;
}
+ .volto-hydra-format-button {
+ border-radius: 5px;
+ margin: 1px;
+ display: none;
+ }
+ .volto-hydra-format-button.show {
+ display: block !important;
+ }
+ .volto-hydra-format-button.active,
+ .volto-hydra-format-button:hover {
+ background-color: #ddd;
+ }
.volto-hydra-drag-button {
cursor: default;
background: #E4E8EC;
@@ -598,7 +1026,7 @@ class Bridge {
display: none;
position: absolute;
top: 100%;
- right: -200%;
+ right: -80%;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
@@ -717,6 +1145,9 @@ const dragSVG = `
`;
+const boldSVG = ``;
+const italicSVG = ``;
+const delSVG = ``;
const addSVG = ``;
const threeDotsSVG = `