diff --git a/README.md b/README.md
index 70540d2..d045c60 100644
--- a/README.md
+++ b/README.md
@@ -75,16 +75,108 @@ The plugin also integrates with the [htmlfy](https://github.com/j4w8n/htmlfy#rea
## Configuration
-Administradors can set these options
+
+### Admin Settings
+
+This section describes the configurable admin settings available for the Tiny CodePro plugin, which enable fine-grained control over valid HTML structures, element nesting, and custom elements in the editor.
-Additionally, the capability `tiny/codepro:viewplugin` controls visibility for specific roles.
+For more information, you can read the [Content filtering options](https://www.tiny.cloud/docs/tinymce/latest/content-filtering/) in TinyMCE documentation page.
+
+---
+
+#### 🔧 `tiny_codepro | extendedvalidelements`
+
+**Default:**
+```
+*[*],svg[*],math[*],script[*],style[*]
+```
+
+**Description:**
+Specifies which HTML elements and attributes are considered valid in the editor. Use the format:
+`tag[attr1|attr2|...],tag[*]` to allow specific attributes, or `[*]` to allow all.
+
+**Example:**
+```
+*[*],svg[*],math[*],script[*]
+```
+
+This example allows all attributes on all tags, as well as unrestricted use of ``, ``, and `',
- expected: `
\n${marker}`,
+ expected: `
\n${htmlMarker}`,
pos: 23,
},
{
name: 'cursor at ',
- expected: `
\n${marker}`,
+ expected: `
\n${htmlMarker}`,
pos: 15,
},
{
name: 'cursor inside \nfirst bold
',
- expected: `${marker}\nfirst bold
`,
+ expected: `${htmlMarker}\nfirst bold
`,
pos: 10,
},
{
name: 'cursor just before closing tag',
doc: 'Bold
',
- expected: `Bold${marker}
`,
+ expected: `Bold${htmlMarker}
`,
pos: 14, // right before
tagName: 'StartCloseTag'
},
@@ -325,83 +357,83 @@ describe('CodeProEditor - Marker Insertion (atElement)', () => {
{
name: 'cursor inside (should skip)',
doc: '
',
- expected: `${marker}
`,
+ expected: `${htmlMarker}
`,
pos: 15
},
{
name: 'cursor inside (should skip)',
doc: 'x
',
- expected: `${marker}x
`,
+ expected: `${htmlMarker}x
`,
pos: 12
},
{
name: 'inside nested valid tag but wrapped in (should skip 1)',
doc: ' ',
- expected: `${marker} `,
+ expected: `${htmlMarker} `,
pos: 18
},
{
name: 'inside nested valid tag but wrapped in (should skip 2)',
doc: ' ',
- expected: `${marker} `,
+ expected: `${htmlMarker} `,
pos: 20
},
{
name: 'deep inside nested
',
- expected: ``,
+ expected: ``,
pos: 20
},
{
name: 'fallback to before ',
- expected: `abc${marker}
`,
+ expected: `abc${htmlMarker}
`,
pos: 10
},
{
name: 'cursor in safe tag but within attribute (should fallback)',
doc: '',
- expected: ``,
+ expected: ``,
pos: 12
},
// More self-closing cases
{
name: 'cursor at self-closing tag ',
doc: 'line next
',
- expected: `line${marker} next
`,
+ expected: `line${htmlMarker} next
`,
pos: 11,
tagName: 'TagName'
},
{
name: 'cursor at empty container',
doc: '
',
- expected: `${marker}
`,
+ expected: `${htmlMarker}
`,
pos: 5,
tagName: 'EndTag'
},
{
name: 'cursor inside nested disallowed tags (script in title)',
doc: ' ',
- expected: `${marker} `,
+ expected: `${htmlMarker} `,
pos: 25,
},
{
name: 'cursor between sibling paragraphs',
doc: '',
- expected: ``,
+ expected: ``,
pos: 17,
tagName: 'EndTag'
},
{
name: 'document already contains marker (gets removed when doc set) 1',
- doc: `@
`,
- expected: `@
`,
+ doc: `@a
`,
+ expected: `${htmlMarker}a
`,
pos: 5,
},
{
name: 'document already contains marker (gets removed when doc set) 2',
doc: `@
`,
- expected: `@
`,
+ expected: `${htmlMarker}
`,
pos: 2,
}
];
@@ -419,4 +451,149 @@ describe('CodeProEditor - Marker Insertion (atElement)', () => {
expectMarker(html, posPresent, expected, tagName);
});
});
-});
\ No newline at end of file
+});
+
+
+describe('CodeProEditor - Advanced Marker Insertion (atElement)', () => {
+ let editor;
+ const htmlMarker = ` `;
+
+ const createEditor = (doc, pos) => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ editor = new CodeProEditor(container, { doc, commands: {} });
+ editor.setSelection({ anchor: pos });
+ return editor;
+ };
+
+ const destroyEditor = () => editor?.destroy();
+
+ afterEach(() => {
+ destroyEditor();
+ });
+
+ const newTests = [
+ // --- Strict tables tests ---
+ {
+ name: 'cursor inside ',
+ doc: '',
+ pos: 26, //
+ expected: ``
+ },
+ {
+ name: 'cursor between and (should place before closing tag)',
+ doc: '',
+ pos: 29, //
+ expected: ``
+ },
+ {
+ name: 'cursor between and (invalid position, should find nearest valid)',
+ doc: '',
+ pos: 19, //
+ expected: ``
+ },
+ {
+ name: 'cursor between and (invalid position, should find nearest valid)',
+ doc: '',
+ pos: 35, //
+ expected: ``
+ },
+
+ // --- Tests with lists ---
+ {
+ name: 'cursor inside ',
+ doc: '',
+ pos: 10, //
+ expected: `It${htmlMarker}em 1 Item 2 `
+ },
+ {
+ name: 'cursor between tags (should place after first item)',
+ doc: '',
+ pos: 18, //
+ expected: `Item 1${htmlMarker} Item 2 `
+ },
+ {
+ name: 'cursor inside invalid position (should place before first li)',
+ doc: '',
+ pos: 4, //
+ expected: ``
+ },
+
+ // --- Tests with selfclosing tags ---
+ {
+ name: 'cursor after an tag',
+ doc: 'Some text and more.
',
+ pos: 27, // Some text and more.
+ expected: `Some text ${htmlMarker} and more.
`
+ },
+ {
+ name: 'cursor before an tag',
+ doc: 'Name:
',
+ pos: 10, // Name: <|input type="text"/>
+ expected: `Name: ${htmlMarker}
`
+ },
+
+ // --- Bad formatted HTML ---
+ {
+ name: 'unclosed tag',
+ doc: '
First paragraph
Second paragraph',
+ pos: 20, //
First paragraph
Second paragraph
+ expected: `
First paragraph${htmlMarker}
Second paragraph
`
+ },
+ {
+ name: 'cursor after unclosed tag',
+ doc: '
',
+ pos: 20, //
+ expected: `unclosed tag${htmlMarker}
`
+ },
+
+ // --- Tests including white spaces and line breaks ---
+ {
+ name: 'lots of whitespace between elements',
+ doc: '',
+ pos: 16, // Before "Hello"
+ expected: `\n
\n ${htmlMarker}Hello\n
\n
`
+ },
+ {
+ name: 'cursor on a blank line between tags',
+ doc: '',
+ pos: 22, //
+ expected: `\n
First
\n ${htmlMarker} \n
Second
\n
`
+ },
+
+ // --- Tests with definition lists (dl, dt, dd) ---
+ {
+ name: 'cursor inside ',
+ doc: ' Term Definition ',
+ pos: 10, // Te|rm Definition
+ expected: `Te${htmlMarker}rm Definition `
+ },
+ {
+ name: 'cursor between and (invalid, should place after dt)',
+ doc: 'Term Definition ',
+ pos: 17, // Term |Definition
+ expected: `Term${htmlMarker} Definition `
+ },
+ {
+ name: 'cursor between and (invalid, should place inside dd)',
+ doc: 'Definition ',
+ pos: 15, // Definition
+ expected: `${htmlMarker}Definition `
+ },
+ // --- Tests within textarea ---
+ {
+ name: 'cursor inside textarea',
+ doc: '',
+ pos: 15, //
+ expected: `${htmlMarker}`
+ },
+ ];
+
+ newTests.forEach(({ name, doc, pos, expected }) => {
+ it(`should handle: ${name}`, () => {
+ const editor = createEditor(doc, pos);
+ const htmlWithMarker = editor.getValue(CodeProEditor.MarkerType.atElement);
+ expect(htmlWithMarker).toBe(expected);
+ });
+ });
+});
diff --git a/libs/codemirror/cursorsync.mjs b/libs/codemirror/cursorsync.mjs
index d130b4a..963e3e6 100644
--- a/libs/codemirror/cursorsync.mjs
+++ b/libs/codemirror/cursorsync.mjs
@@ -24,20 +24,6 @@ import { EditorView } from "@codemirror/view";
import { Transaction } from '@codemirror/state';
import { SearchCursor } from '@codemirror/search';
import { syntaxTree } from '@codemirror/language';
-import { getTagNameFromCursor } from './treeutils';
-
-const disallowedTags = new Set([
- "script", "style", "textarea", "title", "noscript",
- "option", "optgroup", "select",
- "svg", "math", "object", "iframe",
- "head", "meta", "link", "base", "source", "track", "param",
- "img", "input", "br", "hr", "col", "embed", "area", "wbr"
-]);
-
-const disallowedContexts = new Set([
- "ScriptText", "StyleText", "Comment", "CommentText", "CommentBlock", "Attribute", "TagName",
- "StartTag", "EndTag", "MismatchedCloseTag", "ObjectElement", "SvgElement"
-]);
/**
* Class responsible for synchronizing cursor-based interactions
@@ -45,40 +31,46 @@ const disallowedContexts = new Set([
*/
export class CursorSync {
/**
- * Creates an instance of CursorSync.
- *
- * @param {EditorView} editorView - The CodeMirror editor view instance.
- * @param {string} marker - The marker string to insert at cursor/element positions.
- */
- constructor(editorView, marker) {
+ * Creates an instance of CursorSync.
+ *
+ * @param {EditorView} editorView - The CodeMirror editor view instance.
+ * @param {string} marker - The marker string to insert at cursor/element positions.
+ */
+ constructor(editorView, marker, markerClass) {
this.editorView = editorView;
this.marker = marker;
+ this.markerClass = markerClass;
}
/**
* Scrolls the editor view to the position of the marker.
* If the marker is not found, scrolls the current view into focus.
+ * @param {number} [head] - If the head position is passed it will not look for any markers.
*/
- scrollToCaretPosition() {
- const state = this.editorView.state;
- const cursor = new SearchCursor(state.doc, this.marker, 0, state.doc.length);
+ scrollToCaretPosition(head) {
const changes = [];
- let firstMatch = null;
- while (!cursor.next().done) {
- const value = cursor.value;
- if (!cursor.value) {
- continue;
- }
- if (!firstMatch) {
- firstMatch = value;
+ if (!head) {
+ // Look for the marker.
+ const state = this.editorView.state;
+ const cursor = new SearchCursor(state.doc, this.marker, 0, state.doc.length);
+ while (!cursor.next().done) {
+ const value = cursor.value;
+ if (!cursor.value) {
+ continue;
+ }
+ if (!head) {
+ // Stores the position of the first marker found
+ head = value.from;
+ }
+ // To deletes all markers
+ changes.push({ from: value.from, to: value.to, insert: '' });
}
- changes.push({ from: value.from, to: value.to, insert: '' });
}
- if (firstMatch) {
+ if (head) {
this.editorView.dispatch({
changes,
- selection: { anchor: firstMatch.from },
- effects: EditorView.scrollIntoView(firstMatch.from, { y: "center" }),
+ selection: { anchor: head },
+ effects: EditorView.scrollIntoView(head, { y: "center" }),
annotations: [Transaction.addToHistory.of(false)]
});
} else {
@@ -98,123 +90,150 @@ export class CursorSync {
*/
getValueWithMarkerAtCursor() {
const cursor = this.editorView.state.selection.main.head;
- this.editorView.dispatch({
- changes: { from: cursor, insert: this.marker },
- annotations: [Transaction.addToHistory.of(false)]
- });
-
- const html = this.editorView.state.doc.toString();
- if (cursor !== null) {
- this.editorView.dispatch({
- changes: { from: cursor, to: cursor + 1, insert: '' },
- annotations: [Transaction.addToHistory.of(false)]
- });
- }
- return html;
+ const doc = this.editorView.state.doc.toString();
+ return doc.slice(0, cursor) + this.marker + doc.slice(cursor);
}
/**
- * Attempts to insert the marker near a valid HTML element based on the
- * current cursor position. Avoids disallowed contexts and falls back to
- * safe parent containers if necessary.
- *
- * @returns {string} The document content with the marker temporarily inserted.
- */
- getValueWithMarkerAtElement() {
- let state = this.editorView.state;
- const head = state.selection.main.head;
+ * Finds a safe insertion offset using a reliable linear tree traversal.
+ *
+ * This method traverses the tree from the beginning up to the cursor position,
+ * keeping track of the end position of the last "Text" node it encounters.
+ *
+ * This approach has proven to be the most robust and reliable, avoiding
+ * edge navigation issues that previously caused failures.
+ *
+ * @param {EditorState} state - The current state of CodeMirror.
+ * @param {number} head - The cursor position.
+ * @returns {number} A numeric offset guaranteed to be safe for text marker to be placed.
+ */
+ findSafeInsertionOffset(state, head) {
const tree = syntaxTree(state);
- let currentNode = tree.resolve(head, -1);
- const doc = state.doc;
-
- const cursor = currentNode.cursor();
- let pos = null;
- let firstRun = true;
- do {
- const nodeName = cursor.name;
- if (nodeName === "Text") {
- pos = firstRun ? head : cursor.from;
- break;
- }
- if (["EndTag", "SelfClosingTag"].includes(nodeName)) {
- pos = cursor.to;
- break;
- }
+ // 1. Fast Path: If the cursor is already inside a Text node, the position is perfect.
+ const currentNode = tree.resolve(head, -1);
+ if (currentNode.name === 'Text') {
+ return head;
+ }
- if (["StartTag", "StartCloseTag", "Comment"].includes(nodeName)) {
- pos = cursor.from;
- break;
+ // 2. Backward Search: Look for the last text node *before* the cursor.
+ let lastSeenTextEnd = -1; // Use -1 to indicate "not found"
+ tree.iterate({
+ from: 0,
+ to: head,
+ enter: (node) => {
+ if (node.name === 'Text') {
+ lastSeenTextEnd = node.to;
+ }
}
+ });
- if (nodeName === "Element" && !disallowedContexts.has(nodeName)) {
- pos = cursor.from;
- break;
+ // If we found a text node behind the cursor, that's our safe spot.
+ if (lastSeenTextEnd !== -1) {
+ return lastSeenTextEnd;
+ }
+
+ // 3. Forward Search: If no text was found behind the cursor, search forward.
+ // This handles your exact case: Cell 1...
+ let firstSeenTextStart = -1;
+ tree.iterate({
+ from: head,
+ enter: (node) => {
+ if (node.name === 'Text') {
+ if (firstSeenTextStart === -1) { // We only care about the *first* one we find
+ firstSeenTextStart = node.from;
+ }
+ }
}
+ });
+
+ if (firstSeenTextStart !== -1) {
+ return firstSeenTextStart;
+ }
+
+ // Final fallback: If there is no text anywhere in the document, return 0.
+ return 0;
+ }
+
- if (cursor.nextSibling() && cursor.name === "Element") {
- pos = cursor.from;
+ /**
+ * Inserta un marcador HTML final utilizando un marcador de texto provisional
+ * y validando la estructura a través de la API del DOM.
+ */
+ getHtmlWithHybridMarker(html, initialOffset) {
+ const DISALLOWED_PARENTS = new Set(["script", "style", "textarea", "title", "noscript",
+ "option", "optgroup", "select",
+ "svg", "math", "object", "iframe",
+ "head", "meta", "link", "base", "source", "track", "param",
+ "img", "input", "br", "hr", "col", "embed", "area", "wbr"]);
+
+ // 1. Insert a text marker at the best position found so far
+ const htmlWithTextMarker = html.slice(0, initialOffset) + this.marker + html.slice(initialOffset);
+
+ // 2. Parse the DOM
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(htmlWithTextMarker, 'text/html');
+ const body = doc.body;
+
+ // 3. Find where the text marker is placed
+ const walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT);
+ let textNodeWithMarker = null;
+ while (walker.nextNode()) {
+ if (walker.currentNode.nodeValue.includes(this.marker)) {
+ textNodeWithMarker = walker.currentNode;
break;
}
- cursor.prevSibling();
- firstRun = false;
- } while (cursor.parent());
+ }
- if (pos == null) {
- return doc.toString();
+ if (!textNodeWithMarker) {
+ return html; // No marker found, so return the initial html.
}
- const { anyDisallowedFound, safeContainer } = this._getSafeRootedContainer(currentNode, doc);
+ // Divide the TextNode with the marker (to remove the textMarker)
+ const [nodeBefore, nodeAfter] = textNodeWithMarker.nodeValue.split(this.marker);
+ textNodeWithMarker.nodeValue = nodeBefore;
+ const newNodeAfter = document.createTextNode(nodeAfter);
+ textNodeWithMarker.parentElement.insertBefore(newNodeAfter, textNodeWithMarker.nextSibling);
- if (anyDisallowedFound && safeContainer) {
- pos = safeContainer.from;
- } else if (anyDisallowedFound) {
- return doc.toString();
- }
+ // 4. Validate the context and find the final insertion point for the SPAN marker.
+ let insertionPoint = newNodeAfter;
+ let parent = insertionPoint.parentElement;
- this.editorView.dispatch({
- changes: { from: pos, to: pos, insert: this.marker },
- annotations: [Transaction.addToHistory.of(false)]
- });
- state = this.editorView.state;
+ while (parent && parent !== body) {
+ if (DISALLOWED_PARENTS.has(parent.tagName?.toLowerCase())) {
+ insertionPoint = parent; // Move the insertion point before the forbidden element.
+ break;
+ }
+ parent = parent.parentElement;
+ }
- const html = state.doc.toString();
+ // 5. Create and insert the final HTML marker that TinyMCE will be able to understand.
+ const finalMarker = doc.createElement('span');
+ finalMarker.classList.add(this.markerClass);
+ finalMarker.innerHTML = ' ';
- this.editorView.dispatch({
- changes: { from: pos, to: pos + 1, insert: '' },
- annotations: [Transaction.addToHistory.of(false)]
- });
- state = this.editorView.state;
+ insertionPoint.parentElement.insertBefore(finalMarker, insertionPoint);
- return html;
+ // 6. Serialize back to HTML.
+ return body.innerHTML;
}
/**
- * Traverses the syntax tree to find a parent node that is disallowed.
- *
- * @private
- * @param {SyntaxNode} node - The starting syntax tree node.
- * @param {Text} doc - The current document text.
- * @returns {{ anyDisallowedFound: boolean, safeContainer: SyntaxNode | null }} Object with disallowed status and container node.
+ * Attempts to insert the marker near a valid HTML element based on the
+ * current cursor position. Avoids disallowed contexts and falls back to
+ * safe parent containers if necessary.
+ *
+ * @returns {string} The document content with the marker temporarily inserted.
*/
- _getSafeRootedContainer(node, doc) {
- const cursor = node.cursor();
- let safeContainer = null;
- let anyDisallowedFound = false;
- do {
- if (cursor.name === "Element") {
- const tagName = getTagNameFromCursor(cursor.node, doc);
- if (tagName) {
- const name = tagName.toLowerCase();
- if (disallowedTags.has(name)) {
- safeContainer = cursor.node;
- anyDisallowedFound = true;
- }
- }
- }
- } while (cursor.parent());
+ getValueWithMarkerAtElement() {
+ const state = this.editorView.state;
+ const head = state.selection.main.head;
+ const safeInitialOffset = this.findSafeInsertionOffset(state, head);
+ const html = state.doc.toString();
+
+ const finalHtml = this.getHtmlWithHybridMarker(html, safeInitialOffset);
- return { anyDisallowedFound, safeContainer };
+ return finalHtml;
}
/**
@@ -232,4 +251,3 @@ export class CursorSync {
}
}
-
diff --git a/libs/codemirror/package.json b/libs/codemirror/package.json
index 0822cd5..e9f3978 100644
--- a/libs/codemirror/package.json
+++ b/libs/codemirror/package.json
@@ -22,8 +22,8 @@
"@replit/codemirror-css-color-picker": "^6.3.0",
"@replit/codemirror-indentation-markers": "^6.5.3",
"@replit/codemirror-minimap": "^0.5.2",
- "codemirror": "^6.0.1",
- "htmlfy": "^0.7.5"
+ "codemirror": "^6.0.2",
+ "htmlfy": "^1.0.0"
},
"devDependencies": {
"@babel/core": "^7.27.3",
diff --git a/libs/codemirror/treeutils.mjs b/libs/codemirror/treeutils.mjs
deleted file mode 100644
index 78e95b1..0000000
--- a/libs/codemirror/treeutils.mjs
+++ /dev/null
@@ -1,103 +0,0 @@
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle. If not, see .
-
-/**
- * Tiny CodePro plugin. Thin wrapper around CodeMirror 6
- *
- * @module tiny_codepro/plugin
- * @copyright 2024 Josep Mulet Pol
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-/**
- * Recursively finds the tag name from a syntax tree node.
- * @param {Tree} node
- * @param {Text} doc
- * @returns {string|null}
- */
-export function getTagNameFromCursor(node, doc) {
- const cursor = node.cursor();
- function findTagName(c) {
- if (c.name === "TagName") {
- return doc.sliceString(c.from, c.to);
- }
- if (c.firstChild()) {
- do {
- const result = findTagName(c);
- if (result) return result;
- } while (c.nextSibling());
- c.parent();
- }
- return null;
- }
- return findTagName(cursor);
-}
-
-/**
- * Debug utility that prints the full syntax tree.
- * @param {EditorState} state
- */
-export function printFullSyntaxTree(state) {
- const doc = state.doc;
- const tree = syntaxTree(state);
- const cursor = tree.topNode.cursor();
-
- function printNode(c, indent = 0) {
- const padding = " ".repeat(indent);
- const content = doc.sliceString(c.from, c.to).replace(/\n/g, "\\n");
- console.log(`${padding}${c.name} [${c.from}, ${c.to}]: "${content}"`);
-
- if (c.firstChild()) {
- do {
- printNode(c, indent + 1);
- } while (c.nextSibling());
- c.parent();
- }
- }
-
- printNode(cursor);
-}
-
-/**
- * Debug utility that prints the path from a node to the root with siblings.
- * @param {Tree} node
- * @param {Text} doc
- */
-export function printTreePathToRoot(node, doc) {
- const path = [];
- const cursor = node.cursor();
-
- do {
- const siblings = [];
- const siblingCursor = cursor.node.parent?.cursor();
- if (siblingCursor?.firstChild()) {
- do {
- const sFrom = siblingCursor.from;
- const sTo = siblingCursor.to;
- const sText = doc.sliceString(sFrom, sTo).replace(/\n/g, "\\n");
- const isCurrent = siblingCursor.from === cursor.from && siblingCursor.to === cursor.to;
- siblings.push(`${isCurrent ? "\uD83D\uDC49 " : " "}${siblingCursor.name} [${sFrom}, ${sTo}]: "${sText}"`);
- } while (siblingCursor.nextSibling());
- }
-
- path.push(siblings);
- } while (cursor.parent());
-
- console.log("Tree path from node to root with siblings:");
- path.forEach((siblings, level) => {
- const indent = " ".repeat(level);
- siblings.forEach(line => console.log(indent + line));
- });
-}
diff --git a/libs/codemirror/viewmanager.test.js b/libs/codemirror/viewmanager.test.js
index 1a01104..c2202dc 100644
--- a/libs/codemirror/viewmanager.test.js
+++ b/libs/codemirror/viewmanager.test.js
@@ -58,7 +58,8 @@ describe('ViewManager.create', () => {
getRng: jest.fn(() => range),
setRng: jest.fn(),
setCursorLocation: jest.fn(),
- collapse: jest.fn()
+ collapse: jest.fn(),
+ getNode: jest.fn(() => tinyContainer.querySelector('p'))
};
mockTiny = {
@@ -68,6 +69,7 @@ describe('ViewManager.create', () => {
scrollY: 0,
scrollTo: jest.fn()
},
+ getDoc: jest.fn(() => document),
getWin: jest.fn(() => window),
focus: jest.fn(),
dom: {
@@ -82,7 +84,11 @@ describe('ViewManager.create', () => {
}
return elem;
}),
- remove: jest.fn(() => {})
+ remove: jest.fn(() => {}),
+ getParent: jest.fn().mockImplementation((currentNode, tags) => {
+ console.log(currentNode);
+ return currentNode.closest(tags);
+ }),
},
getContent: jest.fn(() => tinyContainer.innerHTML),
setContent: jest.fn((t) => tinyContainer.innerHTML = t),
@@ -109,7 +115,8 @@ describe('ViewManager.create', () => {
it('should create a CodeProEditor instance and attach it to the DOM', async () => {
expect(cm6Container.querySelector('.cm-editor')).toBeNull(); // Before creation
- await viewManager.attachCodeEditor(cm6Container);
+ const docHead = await viewManager.loadDocInfo();
+ await viewManager.attachCodeEditor(cm6Container, docHead);
expect(viewManager.codeEditor).toBeInstanceOf(CodeProEditor);
expect(cm6Container.querySelector('.cm-editor')).not.toBeNull(); // Editor attached
// expect that the head of the cmEditor to be in the right place
diff --git a/settings.php b/settings.php
index ad73cfe..6dac841 100644
--- a/settings.php
+++ b/settings.php
@@ -88,7 +88,7 @@
'tiny_codepro/validchildren',
new lang_string('validchildren', $pluginname),
new lang_string('validchildren_def', $pluginname),
- '+button[div|p|span|strong|em],+p[tiny-svg-block],+span[tiny-svg-block]',
+ '+body[script],+button[div|p|span|strong|em],+p[tiny-svg-block],+span[tiny-svg-block]',
PARAM_TEXT
));
@@ -96,7 +96,7 @@
'tiny_codepro/customelements',
new lang_string('customelements', $pluginname),
new lang_string('customelements_def', $pluginname),
- 'script,style,~svg,~tiny-svg-block',
+ 'script,~svg,~tiny-svg-block',
PARAM_TEXT
));
}
diff --git a/thirdpartylibs.xml b/thirdpartylibs.xml
index a4be0bc..541e48a 100644
--- a/thirdpartylibs.xml
+++ b/thirdpartylibs.xml
@@ -3,14 +3,14 @@
amd/src/cm6pro-lazy.js
cm6pro
- 6.0.1
+ 6.0.2
MIT
amd/src/htmlfy-lazy.js
htmlfy
- 0.6.0
+ 1.0.0
MIT
https://github.com/j4w8n/htmlfy
@@ -18,7 +18,7 @@
libs/codemirror/
codemirror
- 6.0.1
+ 6.0.2
MIT
https://github.com/codemirror/basic-setup
diff --git a/upgrade.txt b/upgrade.txt
index 4621a9c..408be89 100644
--- a/upgrade.txt
+++ b/upgrade.txt
@@ -49,9 +49,12 @@ Issue #27: Saving adds additional paragraphs/spans to html content
== 2.1.1 ==
Automatically hide minimap on mobile phones
-Safer cursor synchronization logic
+Updated cursor synchronization logic. Admin setting for controlling the direction of synchronization
+Enable the default code editor shipped by TinyMCE when capability is disabled
Include CONTRIBUTING.md file
== 2.1.2 ==
+Safer cursor synchronization logic
Fix bug not autosaving in panel mode when form is submitted
-New configuration property disableonpagesregex allows to disable the plugin on certain pages
\ No newline at end of file
+New configuration property disableonpagesregex allows to disable the plugin on certain pages
+Updated library versions: htmlfy to 1.0.0 and codemirror to 6.0.2
diff --git a/version.php b/version.php
index 53cbf32..2219f4a 100644
--- a/version.php
+++ b/version.php
@@ -28,4 +28,4 @@
$plugin->release = '2.1.2';
$plugin->requires = 2022112800;
$plugin->maturity = MATURITY_STABLE;
-$plugin->version = 2025062001;
+$plugin->version = 2025080701;