init
This commit is contained in:
15
public/tinymce/src/plugins/searchreplace/demo/html/demo.html
Normal file
15
public/tinymce/src/plugins/searchreplace/demo/html/demo.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Plugin: searchreplace Demo Page</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Plugin: searchreplace Demo Page</h2>
|
||||
<div id="ephox-ui">
|
||||
<textarea name="" id="" cols="30" rows="10" class="tinymce"></textarea>
|
||||
</div>
|
||||
<script src="../../../../../js/tinymce/tinymce.js"></script>
|
||||
<script src="../../../../../scratch/demos/plugins/searchreplace/demo.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Demo.js
|
||||
*
|
||||
* Released under LGPL License.
|
||||
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
declare let tinymce: any;
|
||||
|
||||
tinymce.init({
|
||||
selector: 'textarea.tinymce',
|
||||
theme: 'modern',
|
||||
skin_url: '../../../../../js/tinymce/skins/lightgray',
|
||||
plugins: 'searchreplace code',
|
||||
toolbar: 'searchreplace code',
|
||||
height: 600
|
||||
});
|
||||
|
||||
export {};
|
||||
23
public/tinymce/src/plugins/searchreplace/main/ts/Plugin.ts
Normal file
23
public/tinymce/src/plugins/searchreplace/main/ts/Plugin.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*/
|
||||
|
||||
import { Cell } from '@ephox/katamari';
|
||||
import PluginManager from 'tinymce/core/api/PluginManager';
|
||||
import Api from './api/Api';
|
||||
import Commands from './api/Commands';
|
||||
import Buttons from './ui/Buttons';
|
||||
|
||||
PluginManager.add('searchreplace', function (editor) {
|
||||
const currentIndexState = Cell(-1);
|
||||
|
||||
Commands.register(editor, currentIndexState);
|
||||
Buttons.register(editor, currentIndexState);
|
||||
|
||||
return Api.get(editor, currentIndexState);
|
||||
});
|
||||
|
||||
export default function () { }
|
||||
42
public/tinymce/src/plugins/searchreplace/main/ts/api/Api.ts
Normal file
42
public/tinymce/src/plugins/searchreplace/main/ts/api/Api.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*/
|
||||
|
||||
import Actions from '../core/Actions';
|
||||
|
||||
const get = function (editor, currentIndexState) {
|
||||
const done = function (keepEditorSelection) {
|
||||
return Actions.done(editor, currentIndexState, keepEditorSelection);
|
||||
};
|
||||
|
||||
const find = function (text, matchCase, wholeWord) {
|
||||
return Actions.find(editor, currentIndexState, text, matchCase, wholeWord);
|
||||
};
|
||||
|
||||
const next = function () {
|
||||
return Actions.next(editor, currentIndexState);
|
||||
};
|
||||
|
||||
const prev = function () {
|
||||
return Actions.prev(editor, currentIndexState);
|
||||
};
|
||||
|
||||
const replace = function (text, forward, all) {
|
||||
return Actions.replace(editor, currentIndexState, text, forward, all);
|
||||
};
|
||||
|
||||
return {
|
||||
done,
|
||||
find,
|
||||
next,
|
||||
prev,
|
||||
replace
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
get
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*/
|
||||
|
||||
import Dialog from '../ui/Dialog';
|
||||
|
||||
const register = function (editor, currentIndexState) {
|
||||
editor.addCommand('SearchReplace', function () {
|
||||
Dialog.open(editor, currentIndexState);
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
register
|
||||
};
|
||||
239
public/tinymce/src/plugins/searchreplace/main/ts/core/Actions.ts
Normal file
239
public/tinymce/src/plugins/searchreplace/main/ts/core/Actions.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*/
|
||||
|
||||
import Tools from 'tinymce/core/api/util/Tools';
|
||||
import FindReplaceText from './FindReplaceText';
|
||||
|
||||
const getElmIndex = function (elm) {
|
||||
const value = elm.getAttribute('data-mce-index');
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return '' + value;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const markAllMatches = function (editor, currentIndexState, regex) {
|
||||
let node, marker;
|
||||
|
||||
marker = editor.dom.create('span', {
|
||||
'data-mce-bogus': 1
|
||||
});
|
||||
|
||||
marker.className = 'mce-match-marker'; // IE 7 adds class="mce-match-marker" and class=mce-match-marker
|
||||
node = editor.getBody();
|
||||
|
||||
done(editor, currentIndexState, false);
|
||||
|
||||
return FindReplaceText.findAndReplaceDOMText(regex, node, marker, false, editor.schema);
|
||||
};
|
||||
|
||||
const unwrap = function (node) {
|
||||
const parentNode = node.parentNode;
|
||||
|
||||
if (node.firstChild) {
|
||||
parentNode.insertBefore(node.firstChild, node);
|
||||
}
|
||||
|
||||
node.parentNode.removeChild(node);
|
||||
};
|
||||
|
||||
const findSpansByIndex = function (editor, index) {
|
||||
let nodes;
|
||||
const spans = [];
|
||||
|
||||
nodes = Tools.toArray(editor.getBody().getElementsByTagName('span'));
|
||||
if (nodes.length) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const nodeIndex = getElmIndex(nodes[i]);
|
||||
|
||||
if (nodeIndex === null || !nodeIndex.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nodeIndex === index.toString()) {
|
||||
spans.push(nodes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return spans;
|
||||
};
|
||||
|
||||
const moveSelection = function (editor, currentIndexState, forward) {
|
||||
let testIndex = currentIndexState.get();
|
||||
const dom = editor.dom;
|
||||
|
||||
forward = forward !== false;
|
||||
|
||||
if (forward) {
|
||||
testIndex++;
|
||||
} else {
|
||||
testIndex--;
|
||||
}
|
||||
|
||||
dom.removeClass(findSpansByIndex(editor, currentIndexState.get()), 'mce-match-marker-selected');
|
||||
|
||||
const spans = findSpansByIndex(editor, testIndex);
|
||||
if (spans.length) {
|
||||
dom.addClass(findSpansByIndex(editor, testIndex), 'mce-match-marker-selected');
|
||||
editor.selection.scrollIntoView(spans[0]);
|
||||
return testIndex;
|
||||
}
|
||||
|
||||
return -1;
|
||||
};
|
||||
|
||||
const removeNode = function (dom, node) {
|
||||
const parent = node.parentNode;
|
||||
|
||||
dom.remove(node);
|
||||
|
||||
if (dom.isEmpty(parent)) {
|
||||
dom.remove(parent);
|
||||
}
|
||||
};
|
||||
|
||||
const find = function (editor, currentIndexState, text, matchCase, wholeWord) {
|
||||
text = text.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
|
||||
text = text.replace(/\s/g, '[^\\S\\r\\n]');
|
||||
text = wholeWord ? '\\b' + text + '\\b' : text;
|
||||
|
||||
const count = markAllMatches(editor, currentIndexState, new RegExp(text, matchCase ? 'g' : 'gi'));
|
||||
|
||||
if (count) {
|
||||
currentIndexState.set(-1);
|
||||
currentIndexState.set(moveSelection(editor, currentIndexState, true));
|
||||
}
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
const next = function (editor, currentIndexState) {
|
||||
const index = moveSelection(editor, currentIndexState, true);
|
||||
|
||||
if (index !== -1) {
|
||||
currentIndexState.set(index);
|
||||
}
|
||||
};
|
||||
|
||||
const prev = function (editor, currentIndexState) {
|
||||
const index = moveSelection(editor, currentIndexState, false);
|
||||
|
||||
if (index !== -1) {
|
||||
currentIndexState.set(index);
|
||||
}
|
||||
};
|
||||
|
||||
const isMatchSpan = function (node) {
|
||||
const matchIndex = getElmIndex(node);
|
||||
|
||||
return matchIndex !== null && matchIndex.length > 0;
|
||||
};
|
||||
|
||||
const replace = function (editor, currentIndexState, text, forward?, all?) {
|
||||
let i, nodes, node, matchIndex, currentMatchIndex, nextIndex = currentIndexState.get(), hasMore;
|
||||
|
||||
forward = forward !== false;
|
||||
|
||||
node = editor.getBody();
|
||||
nodes = Tools.grep(Tools.toArray(node.getElementsByTagName('span')), isMatchSpan);
|
||||
for (i = 0; i < nodes.length; i++) {
|
||||
const nodeIndex = getElmIndex(nodes[i]);
|
||||
|
||||
matchIndex = currentMatchIndex = parseInt(nodeIndex, 10);
|
||||
if (all || matchIndex === currentIndexState.get()) {
|
||||
if (text.length) {
|
||||
nodes[i].firstChild.nodeValue = text;
|
||||
unwrap(nodes[i]);
|
||||
} else {
|
||||
removeNode(editor.dom, nodes[i]);
|
||||
}
|
||||
|
||||
while (nodes[++i]) {
|
||||
matchIndex = parseInt(getElmIndex(nodes[i]), 10);
|
||||
|
||||
if (matchIndex === currentMatchIndex) {
|
||||
removeNode(editor.dom, nodes[i]);
|
||||
} else {
|
||||
i--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (forward) {
|
||||
nextIndex--;
|
||||
}
|
||||
} else if (currentMatchIndex > currentIndexState.get()) {
|
||||
nodes[i].setAttribute('data-mce-index', currentMatchIndex - 1);
|
||||
}
|
||||
}
|
||||
|
||||
currentIndexState.set(nextIndex);
|
||||
|
||||
if (forward) {
|
||||
hasMore = hasNext(editor, currentIndexState);
|
||||
next(editor, currentIndexState);
|
||||
} else {
|
||||
hasMore = hasPrev(editor, currentIndexState);
|
||||
prev(editor, currentIndexState);
|
||||
}
|
||||
|
||||
return !all && hasMore;
|
||||
};
|
||||
|
||||
const done = function (editor, currentIndexState, keepEditorSelection?) {
|
||||
let i, nodes, startContainer, endContainer;
|
||||
|
||||
nodes = Tools.toArray(editor.getBody().getElementsByTagName('span'));
|
||||
for (i = 0; i < nodes.length; i++) {
|
||||
const nodeIndex = getElmIndex(nodes[i]);
|
||||
|
||||
if (nodeIndex !== null && nodeIndex.length) {
|
||||
if (nodeIndex === currentIndexState.get().toString()) {
|
||||
if (!startContainer) {
|
||||
startContainer = nodes[i].firstChild;
|
||||
}
|
||||
|
||||
endContainer = nodes[i].firstChild;
|
||||
}
|
||||
|
||||
unwrap(nodes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (startContainer && endContainer) {
|
||||
const rng = editor.dom.createRng();
|
||||
rng.setStart(startContainer, 0);
|
||||
rng.setEnd(endContainer, endContainer.data.length);
|
||||
|
||||
if (keepEditorSelection !== false) {
|
||||
editor.selection.setRng(rng);
|
||||
}
|
||||
|
||||
return rng;
|
||||
}
|
||||
};
|
||||
|
||||
const hasNext = function (editor, currentIndexState) {
|
||||
return findSpansByIndex(editor, currentIndexState.get() + 1).length > 0;
|
||||
};
|
||||
|
||||
const hasPrev = function (editor, currentIndexState) {
|
||||
return findSpansByIndex(editor, currentIndexState.get() - 1).length > 0;
|
||||
};
|
||||
|
||||
export default {
|
||||
done,
|
||||
find,
|
||||
next,
|
||||
prev,
|
||||
replace,
|
||||
hasNext,
|
||||
hasPrev
|
||||
};
|
||||
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*/
|
||||
|
||||
/*jshint smarttabs:true, undef:true, unused:true, latedef:true, curly:true, bitwise:true */
|
||||
|
||||
/*eslint no-labels:0, no-constant-condition: 0 */
|
||||
|
||||
function isContentEditableFalse(node) {
|
||||
return node && node.nodeType === 1 && node.contentEditable === 'false';
|
||||
}
|
||||
|
||||
// Based on work developed by: James Padolsey http://james.padolsey.com
|
||||
// released under UNLICENSE that is compatible with LGPL
|
||||
// TODO: Handle contentEditable edgecase:
|
||||
// <p>text<span contentEditable="false">text<span contentEditable="true">text</span>text</span>text</p>
|
||||
function findAndReplaceDOMText(regex, node, replacementNode, captureGroup, schema) {
|
||||
let m;
|
||||
const matches = [];
|
||||
let text, count = 0, doc;
|
||||
let blockElementsMap, hiddenTextElementsMap, shortEndedElementsMap;
|
||||
|
||||
doc = node.ownerDocument;
|
||||
blockElementsMap = schema.getBlockElements(); // H1-H6, P, TD etc
|
||||
hiddenTextElementsMap = schema.getWhiteSpaceElements(); // TEXTAREA, PRE, STYLE, SCRIPT
|
||||
shortEndedElementsMap = schema.getShortEndedElements(); // BR, IMG, INPUT
|
||||
|
||||
function getMatchIndexes(m, captureGroup) {
|
||||
captureGroup = captureGroup || 0;
|
||||
|
||||
if (!m[0]) {
|
||||
throw new Error('findAndReplaceDOMText cannot handle zero-length matches');
|
||||
}
|
||||
|
||||
let index = m.index;
|
||||
|
||||
if (captureGroup > 0) {
|
||||
const cg = m[captureGroup];
|
||||
|
||||
if (!cg) {
|
||||
throw new Error('Invalid capture group');
|
||||
}
|
||||
|
||||
index += m[0].indexOf(cg);
|
||||
m[0] = cg;
|
||||
}
|
||||
|
||||
return [index, index + m[0].length, [m[0]]];
|
||||
}
|
||||
|
||||
function getText(node) {
|
||||
let txt;
|
||||
|
||||
if (node.nodeType === 3) {
|
||||
return node.data;
|
||||
}
|
||||
|
||||
if (hiddenTextElementsMap[node.nodeName] && !blockElementsMap[node.nodeName]) {
|
||||
return '';
|
||||
}
|
||||
|
||||
txt = '';
|
||||
|
||||
if (isContentEditableFalse(node)) {
|
||||
return '\n';
|
||||
}
|
||||
|
||||
if (blockElementsMap[node.nodeName] || shortEndedElementsMap[node.nodeName]) {
|
||||
txt += '\n';
|
||||
}
|
||||
|
||||
if ((node = node.firstChild)) {
|
||||
do {
|
||||
txt += getText(node);
|
||||
} while ((node = node.nextSibling));
|
||||
}
|
||||
|
||||
return txt;
|
||||
}
|
||||
|
||||
function stepThroughMatches(node, matches, replaceFn) {
|
||||
let startNode, endNode, startNodeIndex,
|
||||
endNodeIndex, innerNodes = [], atIndex = 0, curNode = node,
|
||||
matchLocation = matches.shift(), matchIndex = 0;
|
||||
|
||||
out: while (true) {
|
||||
if (blockElementsMap[curNode.nodeName] || shortEndedElementsMap[curNode.nodeName] || isContentEditableFalse(curNode)) {
|
||||
atIndex++;
|
||||
}
|
||||
|
||||
if (curNode.nodeType === 3) {
|
||||
if (!endNode && curNode.length + atIndex >= matchLocation[1]) {
|
||||
// We've found the ending
|
||||
endNode = curNode;
|
||||
endNodeIndex = matchLocation[1] - atIndex;
|
||||
} else if (startNode) {
|
||||
// Intersecting node
|
||||
innerNodes.push(curNode);
|
||||
}
|
||||
|
||||
if (!startNode && curNode.length + atIndex > matchLocation[0]) {
|
||||
// We've found the match start
|
||||
startNode = curNode;
|
||||
startNodeIndex = matchLocation[0] - atIndex;
|
||||
}
|
||||
|
||||
atIndex += curNode.length;
|
||||
}
|
||||
|
||||
if (startNode && endNode) {
|
||||
curNode = replaceFn({
|
||||
startNode,
|
||||
startNodeIndex,
|
||||
endNode,
|
||||
endNodeIndex,
|
||||
innerNodes,
|
||||
match: matchLocation[2],
|
||||
matchIndex
|
||||
});
|
||||
|
||||
// replaceFn has to return the node that replaced the endNode
|
||||
// and then we step back so we can continue from the end of the
|
||||
// match:
|
||||
atIndex -= (endNode.length - endNodeIndex);
|
||||
startNode = null;
|
||||
endNode = null;
|
||||
innerNodes = [];
|
||||
matchLocation = matches.shift();
|
||||
matchIndex++;
|
||||
|
||||
if (!matchLocation) {
|
||||
break; // no more matches
|
||||
}
|
||||
} else if ((!hiddenTextElementsMap[curNode.nodeName] || blockElementsMap[curNode.nodeName]) && curNode.firstChild) {
|
||||
if (!isContentEditableFalse(curNode)) {
|
||||
// Move down
|
||||
curNode = curNode.firstChild;
|
||||
continue;
|
||||
}
|
||||
} else if (curNode.nextSibling) {
|
||||
// Move forward:
|
||||
curNode = curNode.nextSibling;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Move forward or up:
|
||||
while (true) {
|
||||
if (curNode.nextSibling) {
|
||||
curNode = curNode.nextSibling;
|
||||
break;
|
||||
} else if (curNode.parentNode !== node) {
|
||||
curNode = curNode.parentNode;
|
||||
} else {
|
||||
break out;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the actual replaceFn which splits up text nodes
|
||||
* and inserts the replacement element.
|
||||
*/
|
||||
function genReplacer(nodeName) {
|
||||
let makeReplacementNode;
|
||||
|
||||
if (typeof nodeName !== 'function') {
|
||||
const stencilNode = nodeName.nodeType ? nodeName : doc.createElement(nodeName);
|
||||
|
||||
makeReplacementNode = function (fill, matchIndex) {
|
||||
const clone = stencilNode.cloneNode(false);
|
||||
|
||||
clone.setAttribute('data-mce-index', matchIndex);
|
||||
|
||||
if (fill) {
|
||||
clone.appendChild(doc.createTextNode(fill));
|
||||
}
|
||||
|
||||
return clone;
|
||||
};
|
||||
} else {
|
||||
makeReplacementNode = nodeName;
|
||||
}
|
||||
|
||||
return function (range) {
|
||||
let before;
|
||||
let after;
|
||||
let parentNode;
|
||||
const startNode = range.startNode;
|
||||
const endNode = range.endNode;
|
||||
const matchIndex = range.matchIndex;
|
||||
|
||||
if (startNode === endNode) {
|
||||
const node = startNode;
|
||||
|
||||
parentNode = node.parentNode;
|
||||
if (range.startNodeIndex > 0) {
|
||||
// Add `before` text node (before the match)
|
||||
before = doc.createTextNode(node.data.substring(0, range.startNodeIndex));
|
||||
parentNode.insertBefore(before, node);
|
||||
}
|
||||
|
||||
// Create the replacement node:
|
||||
const el = makeReplacementNode(range.match[0], matchIndex);
|
||||
parentNode.insertBefore(el, node);
|
||||
if (range.endNodeIndex < node.length) {
|
||||
// Add `after` text node (after the match)
|
||||
after = doc.createTextNode(node.data.substring(range.endNodeIndex));
|
||||
parentNode.insertBefore(after, node);
|
||||
}
|
||||
|
||||
node.parentNode.removeChild(node);
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
// Replace startNode -> [innerNodes...] -> endNode (in that order)
|
||||
before = doc.createTextNode(startNode.data.substring(0, range.startNodeIndex));
|
||||
after = doc.createTextNode(endNode.data.substring(range.endNodeIndex));
|
||||
const elA = makeReplacementNode(startNode.data.substring(range.startNodeIndex), matchIndex);
|
||||
const innerEls = [];
|
||||
|
||||
for (let i = 0, l = range.innerNodes.length; i < l; ++i) {
|
||||
const innerNode = range.innerNodes[i];
|
||||
const innerEl = makeReplacementNode(innerNode.data, matchIndex);
|
||||
innerNode.parentNode.replaceChild(innerEl, innerNode);
|
||||
innerEls.push(innerEl);
|
||||
}
|
||||
|
||||
const elB = makeReplacementNode(endNode.data.substring(0, range.endNodeIndex), matchIndex);
|
||||
|
||||
parentNode = startNode.parentNode;
|
||||
parentNode.insertBefore(before, startNode);
|
||||
parentNode.insertBefore(elA, startNode);
|
||||
parentNode.removeChild(startNode);
|
||||
|
||||
parentNode = endNode.parentNode;
|
||||
parentNode.insertBefore(elB, endNode);
|
||||
parentNode.insertBefore(after, endNode);
|
||||
parentNode.removeChild(endNode);
|
||||
|
||||
return elB;
|
||||
};
|
||||
}
|
||||
|
||||
text = getText(node);
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (regex.global) {
|
||||
while ((m = regex.exec(text))) {
|
||||
matches.push(getMatchIndexes(m, captureGroup));
|
||||
}
|
||||
} else {
|
||||
m = text.match(regex);
|
||||
matches.push(getMatchIndexes(m, captureGroup));
|
||||
}
|
||||
|
||||
if (matches.length) {
|
||||
count = matches.length;
|
||||
stepThroughMatches(node, matches, genReplacer(replacementNode));
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
export default {
|
||||
findAndReplaceDOMText
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*/
|
||||
|
||||
import Dialog from './Dialog';
|
||||
|
||||
const showDialog = function (editor, currentIndexState) {
|
||||
return function () {
|
||||
Dialog.open(editor, currentIndexState);
|
||||
};
|
||||
};
|
||||
|
||||
const register = function (editor, currentIndexState) {
|
||||
editor.addMenuItem('searchreplace', {
|
||||
text: 'Find and replace',
|
||||
shortcut: 'Meta+F',
|
||||
onclick: showDialog(editor, currentIndexState),
|
||||
separator: 'before',
|
||||
context: 'edit'
|
||||
});
|
||||
|
||||
editor.addButton('searchreplace', {
|
||||
tooltip: 'Find and replace',
|
||||
onclick: showDialog(editor, currentIndexState)
|
||||
});
|
||||
|
||||
editor.shortcuts.add('Meta+F', '', showDialog(editor, currentIndexState));
|
||||
};
|
||||
|
||||
export default {
|
||||
register
|
||||
};
|
||||
131
public/tinymce/src/plugins/searchreplace/main/ts/ui/Dialog.ts
Normal file
131
public/tinymce/src/plugins/searchreplace/main/ts/ui/Dialog.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
|
||||
* Licensed under the LGPL or a commercial license.
|
||||
* For LGPL see License.txt in the project root for license information.
|
||||
* For commercial licenses see https://www.tiny.cloud/
|
||||
*/
|
||||
|
||||
import Tools from 'tinymce/core/api/util/Tools';
|
||||
import Actions from '../core/Actions';
|
||||
|
||||
const open = function (editor, currentIndexState) {
|
||||
let last: any = {}, selectedText;
|
||||
editor.undoManager.add();
|
||||
|
||||
selectedText = Tools.trim(editor.selection.getContent({ format: 'text' }));
|
||||
|
||||
function updateButtonStates() {
|
||||
win.statusbar.find('#next').disabled(Actions.hasNext(editor, currentIndexState) === false);
|
||||
win.statusbar.find('#prev').disabled(Actions.hasPrev(editor, currentIndexState) === false);
|
||||
}
|
||||
|
||||
function notFoundAlert() {
|
||||
editor.windowManager.alert('Could not find the specified string.', function () {
|
||||
win.find('#find')[0].focus();
|
||||
});
|
||||
}
|
||||
|
||||
const win = editor.windowManager.open({
|
||||
layout: 'flex',
|
||||
pack: 'center',
|
||||
align: 'center',
|
||||
onClose () {
|
||||
editor.focus();
|
||||
Actions.done(editor, currentIndexState);
|
||||
editor.undoManager.add();
|
||||
},
|
||||
onSubmit (e) {
|
||||
let count, caseState, text, wholeWord;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
caseState = win.find('#case').checked();
|
||||
wholeWord = win.find('#words').checked();
|
||||
|
||||
text = win.find('#find').value();
|
||||
if (!text.length) {
|
||||
Actions.done(editor, currentIndexState, false);
|
||||
win.statusbar.items().slice(1).disabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (last.text === text && last.caseState === caseState && last.wholeWord === wholeWord) {
|
||||
if (!Actions.hasNext(editor, currentIndexState)) {
|
||||
notFoundAlert();
|
||||
return;
|
||||
}
|
||||
|
||||
Actions.next(editor, currentIndexState);
|
||||
updateButtonStates();
|
||||
return;
|
||||
}
|
||||
|
||||
count = Actions.find(editor, currentIndexState, text, caseState, wholeWord);
|
||||
if (!count) {
|
||||
notFoundAlert();
|
||||
}
|
||||
|
||||
win.statusbar.items().slice(1).disabled(count === 0);
|
||||
updateButtonStates();
|
||||
|
||||
last = {
|
||||
text,
|
||||
caseState,
|
||||
wholeWord
|
||||
};
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
text: 'Find', subtype: 'primary', onclick () {
|
||||
win.submit();
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Replace', disabled: true, onclick () {
|
||||
if (!Actions.replace(editor, currentIndexState, win.find('#replace').value())) {
|
||||
win.statusbar.items().slice(1).disabled(true);
|
||||
currentIndexState.set(-1);
|
||||
last = {};
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Replace all', disabled: true, onclick () {
|
||||
Actions.replace(editor, currentIndexState, win.find('#replace').value(), true, true);
|
||||
win.statusbar.items().slice(1).disabled(true);
|
||||
last = {};
|
||||
}
|
||||
},
|
||||
{ type: 'spacer', flex: 1 },
|
||||
{
|
||||
text: 'Prev', name: 'prev', disabled: true, onclick () {
|
||||
Actions.prev(editor, currentIndexState);
|
||||
updateButtonStates();
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Next', name: 'next', disabled: true, onclick () {
|
||||
Actions.next(editor, currentIndexState);
|
||||
updateButtonStates();
|
||||
}
|
||||
}
|
||||
],
|
||||
title: 'Find and replace',
|
||||
items: {
|
||||
type: 'form',
|
||||
padding: 20,
|
||||
labelGap: 30,
|
||||
spacing: 10,
|
||||
items: [
|
||||
{ type: 'textbox', name: 'find', size: 40, label: 'Find', value: selectedText },
|
||||
{ type: 'textbox', name: 'replace', size: 40, label: 'Replace with' },
|
||||
{ type: 'checkbox', name: 'case', text: 'Match case', label: ' ' },
|
||||
{ type: 'checkbox', name: 'words', text: 'Whole words', label: ' ' }
|
||||
]
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
open
|
||||
};
|
||||
@@ -0,0 +1,148 @@
|
||||
import { Pipeline } from '@ephox/agar';
|
||||
import { UnitTest } from '@ephox/bedrock';
|
||||
import { LegacyUnit, TinyLoader } from '@ephox/mcagar';
|
||||
|
||||
import Theme from 'tinymce/themes/modern/Theme';
|
||||
|
||||
import HtmlUtils from '../module/test/HtmlUtils';
|
||||
|
||||
UnitTest.asynctest(
|
||||
'browser.tinymce.plugins.searchreplace.SearchReplacePluginTest',
|
||||
function () {
|
||||
const success = arguments[arguments.length - 2];
|
||||
const failure = arguments[arguments.length - 1];
|
||||
const suite = LegacyUnit.createSuite();
|
||||
Theme();
|
||||
|
||||
suite.test('Find no match', function (editor) {
|
||||
editor.focus();
|
||||
editor.setContent('a');
|
||||
LegacyUnit.equal(0, editor.plugins.searchreplace.find('x'));
|
||||
});
|
||||
|
||||
suite.test('Find single match', function (editor) {
|
||||
editor.setContent('a');
|
||||
LegacyUnit.equal(1, editor.plugins.searchreplace.find('a'));
|
||||
});
|
||||
|
||||
suite.test('Find single match in multiple elements', function (editor) {
|
||||
editor.setContent('t<b>e</b><i>xt</i>');
|
||||
LegacyUnit.equal(1, editor.plugins.searchreplace.find('text'));
|
||||
});
|
||||
|
||||
suite.test('Find single match, match case: true', function (editor) {
|
||||
editor.setContent('a A');
|
||||
LegacyUnit.equal(1, editor.plugins.searchreplace.find('A', true));
|
||||
});
|
||||
|
||||
suite.test('Find single match, whole words: true', function (editor) {
|
||||
editor.setContent('a Ax');
|
||||
LegacyUnit.equal(1, editor.plugins.searchreplace.find('a', false, true));
|
||||
});
|
||||
|
||||
suite.test('Find multiple matches', function (editor) {
|
||||
editor.setContent('a b A');
|
||||
LegacyUnit.equal(2, editor.plugins.searchreplace.find('a'));
|
||||
});
|
||||
|
||||
suite.test('Find and replace single match', function (editor) {
|
||||
editor.setContent('a');
|
||||
editor.plugins.searchreplace.find('a');
|
||||
LegacyUnit.equal(editor.plugins.searchreplace.replace('x'), false);
|
||||
LegacyUnit.equal('<p>x</p>', editor.getContent());
|
||||
});
|
||||
|
||||
suite.test('Find and replace first in multiple matches', function (editor) {
|
||||
editor.setContent('a b a');
|
||||
editor.plugins.searchreplace.find('a');
|
||||
LegacyUnit.equal(editor.plugins.searchreplace.replace('x'), true);
|
||||
LegacyUnit.equal('<p>x b a</p>', editor.getContent());
|
||||
});
|
||||
|
||||
suite.test('Find and replace two consecutive spaces', function (editor) {
|
||||
editor.setContent('a b');
|
||||
editor.plugins.searchreplace.find('a ');
|
||||
LegacyUnit.equal(editor.plugins.searchreplace.replace('x'), false);
|
||||
LegacyUnit.equal('<p>xb</p>', editor.getContent());
|
||||
});
|
||||
|
||||
suite.test('Find and replace consecutive spaces', function (editor) {
|
||||
editor.setContent('a b');
|
||||
editor.plugins.searchreplace.find('a ');
|
||||
LegacyUnit.equal(editor.plugins.searchreplace.replace('x'), false);
|
||||
LegacyUnit.equal('<p>xb</p>', editor.getContent());
|
||||
});
|
||||
|
||||
suite.test('Find and replace all in multiple matches', function (editor) {
|
||||
editor.setContent('a b a');
|
||||
editor.plugins.searchreplace.find('a');
|
||||
LegacyUnit.equal(editor.plugins.searchreplace.replace('x', true, true), false);
|
||||
LegacyUnit.equal('<p>x b x</p>', editor.getContent());
|
||||
});
|
||||
|
||||
suite.test('Find and replace all spaces with new lines', function (editor) {
|
||||
editor.setContent('a b<br/><br/>ab c');
|
||||
editor.plugins.searchreplace.find(' ');
|
||||
LegacyUnit.equal(editor.plugins.searchreplace.replace('x', true, true), false);
|
||||
LegacyUnit.equal('<p>axxxb<br /><br />abxc</p>', editor.getContent());
|
||||
});
|
||||
|
||||
suite.test('Find multiple matches, move to next and replace', function (editor) {
|
||||
editor.setContent('a a');
|
||||
LegacyUnit.equal(2, editor.plugins.searchreplace.find('a'));
|
||||
editor.plugins.searchreplace.next();
|
||||
LegacyUnit.equal(editor.plugins.searchreplace.replace('x'), false);
|
||||
LegacyUnit.equal('<p>a x</p>', editor.getContent());
|
||||
});
|
||||
|
||||
suite.test('Find and replace fragmented match', function (editor) {
|
||||
editor.setContent('<b>te<i>s</i>t</b><b>te<i>s</i>t</b>');
|
||||
editor.plugins.searchreplace.find('test');
|
||||
LegacyUnit.equal(editor.plugins.searchreplace.replace('abc'), true);
|
||||
LegacyUnit.equal(editor.getContent(), '<p><b>abc</b><b>te<i>s</i>t</b></p>');
|
||||
});
|
||||
|
||||
suite.test('Find and replace all fragmented matches', function (editor) {
|
||||
editor.setContent('<b>te<i>s</i>t</b><b>te<i>s</i>t</b>');
|
||||
editor.plugins.searchreplace.find('test');
|
||||
LegacyUnit.equal(editor.plugins.searchreplace.replace('abc', true, true), false);
|
||||
LegacyUnit.equal(editor.getContent(), '<p><b>abc</b><b>abc</b></p>');
|
||||
});
|
||||
|
||||
suite.test('Find multiple matches, move to next and replace backwards', function (editor) {
|
||||
editor.setContent('a a');
|
||||
LegacyUnit.equal(2, editor.plugins.searchreplace.find('a'));
|
||||
editor.plugins.searchreplace.next();
|
||||
LegacyUnit.equal(editor.plugins.searchreplace.replace('x', false), true);
|
||||
LegacyUnit.equal(editor.plugins.searchreplace.replace('y', false), false);
|
||||
LegacyUnit.equal('<p>y x</p>', editor.getContent());
|
||||
});
|
||||
|
||||
suite.test('Find multiple matches and unmark them', function (editor) {
|
||||
editor.setContent('a b a');
|
||||
LegacyUnit.equal(2, editor.plugins.searchreplace.find('a'));
|
||||
editor.plugins.searchreplace.done();
|
||||
LegacyUnit.equal('a', editor.selection.getContent());
|
||||
LegacyUnit.equal(0, editor.getBody().getElementsByTagName('span').length);
|
||||
});
|
||||
|
||||
suite.test('Find multiple matches with pre blocks', function (editor) {
|
||||
editor.getBody().innerHTML = 'abc<pre> abc </pre>abc';
|
||||
LegacyUnit.equal(3, editor.plugins.searchreplace.find('b'));
|
||||
LegacyUnit.equal(HtmlUtils.normalizeHtml(editor.getBody().innerHTML), (
|
||||
'a<span class="mce-match-marker mce-match-marker-selected" data-mce-bogus="1" data-mce-index="0">b</span>c' +
|
||||
'<pre> a<span class="mce-match-marker" data-mce-bogus="1" data-mce-index="1">b</span>c </pre>' +
|
||||
'a<span class="mce-match-marker" data-mce-bogus="1" data-mce-index="2">b</span>c'
|
||||
));
|
||||
});
|
||||
|
||||
TinyLoader.setup(function (editor, onSuccess, onFailure) {
|
||||
Pipeline.async({}, suite.toSteps(editor), onSuccess, onFailure);
|
||||
}, {
|
||||
plugins: 'searchreplace',
|
||||
valid_elements: 'b,i,br',
|
||||
indent: false,
|
||||
skin_url: '/project/js/tinymce/skins/lightgray'
|
||||
}, success, failure);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
Chain, GeneralSteps, Logger, Mouse, Pipeline, Step, UiControls, UiFinder
|
||||
} from '@ephox/agar';
|
||||
import { UnitTest } from '@ephox/bedrock';
|
||||
import { TinyApis, TinyLoader, TinyUi } from '@ephox/mcagar';
|
||||
|
||||
import SearchreplacePlugin from 'tinymce/plugins/searchreplace/Plugin';
|
||||
import ModernTheme from 'tinymce/themes/modern/Theme';
|
||||
|
||||
UnitTest.asynctest('browser.tinymce.plugins.searchreplace.UndoReplaceSpanTest', function () {
|
||||
const success = arguments[arguments.length - 2];
|
||||
const failure = arguments[arguments.length - 1];
|
||||
|
||||
ModernTheme();
|
||||
SearchreplacePlugin();
|
||||
|
||||
const sUndo = function (editor) {
|
||||
return Step.sync(function () {
|
||||
editor.undoManager.undo();
|
||||
});
|
||||
};
|
||||
|
||||
const sRedo = function (editor) {
|
||||
return Step.sync(function () {
|
||||
editor.undoManager.redo();
|
||||
});
|
||||
};
|
||||
|
||||
TinyLoader.setup(function (editor, onSuccess, onFailure) {
|
||||
const tinyApis = TinyApis(editor);
|
||||
const tinyUi = TinyUi(editor);
|
||||
|
||||
Pipeline.async({}, [
|
||||
Logger.t('replace on of three found, undo and redo and there be no matcher spans in editor', GeneralSteps.sequence([
|
||||
tinyApis.sSetContent('<p>cats cats cats</p>'),
|
||||
tinyUi.sClickOnToolbar('click on searchreplace button', 'div[aria-label="Find and replace"] button'),
|
||||
Chain.asStep({}, [
|
||||
Chain.fromParent(tinyUi.cWaitForPopup('wait for dialog', 'div[role="dialog"]'), [
|
||||
Chain.fromChains([
|
||||
UiFinder.cFindIn('label:contains("Find") + input'),
|
||||
UiControls.cSetValue('cats')
|
||||
]),
|
||||
Chain.fromChains([
|
||||
UiFinder.cFindIn('label:contains("Replace with") + input'),
|
||||
UiControls.cSetValue('dogs')
|
||||
]),
|
||||
Chain.fromChains([
|
||||
UiFinder.cFindIn('button:contains("Find")'),
|
||||
Mouse.cClick
|
||||
]),
|
||||
Chain.fromChains([
|
||||
UiFinder.cWaitFor('wait for button to be enabled', 'div[aria-disabled="false"] span:contains("Replace")')
|
||||
]),
|
||||
Chain.fromChains([
|
||||
UiFinder.cFindIn('button:contains("Replace")'),
|
||||
Mouse.cClick
|
||||
]),
|
||||
Chain.fromChains([
|
||||
UiFinder.cFindIn('button.mce-close'),
|
||||
Mouse.cClick
|
||||
])
|
||||
])
|
||||
]),
|
||||
sUndo(editor),
|
||||
tinyApis.sAssertContent('<p>cats cats cats</p>'),
|
||||
sRedo(editor),
|
||||
tinyApis.sAssertContentPresence({ 'span.mce-match-marker': 0 }),
|
||||
tinyApis.sAssertContent('<p>dogs cats cats</p>')
|
||||
]))
|
||||
], onSuccess, onFailure);
|
||||
}, {
|
||||
plugins: 'searchreplace',
|
||||
toolbar: 'searchreplace',
|
||||
skin_url: '/project/js/tinymce/skins/lightgray'
|
||||
}, success, failure);
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import Writer from 'tinymce/core/api/html/Writer';
|
||||
import SaxParser from 'tinymce/core/api/html/SaxParser';
|
||||
|
||||
const cleanHtml = function (html) {
|
||||
return html.toLowerCase().replace(/[\r\n]+/gi, '')
|
||||
.replace(/ (sizcache[0-9]+|sizcache|nodeindex|sizset[0-9]+|sizset|data\-mce\-expando|data\-mce\-selected)="[^"]*"/gi, '')
|
||||
.replace(/<span[^>]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\/span>|<div[^>]+data-mce-bogus[^>]+><\/div>/gi, '')
|
||||
.replace(/ style="([^"]+)"/gi, function (val1, val2) {
|
||||
val2 = val2.replace(/;$/, '');
|
||||
return ' style="' + val2.replace(/\:([^ ])/g, ': $1') + ';"';
|
||||
});
|
||||
};
|
||||
|
||||
const normalizeHtml = function (html) {
|
||||
const writer = Writer();
|
||||
|
||||
SaxParser({
|
||||
validate: false,
|
||||
comment: writer.comment,
|
||||
cdata: writer.cdata,
|
||||
text: writer.text,
|
||||
end: writer.end,
|
||||
pi: writer.pi,
|
||||
doctype: writer.doctype,
|
||||
|
||||
start (name, attrs, empty) {
|
||||
attrs.sort(function (a, b) {
|
||||
if (a.name === b.name) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return a.name > b.name ? 1 : -1;
|
||||
});
|
||||
|
||||
writer.start(name, attrs, empty);
|
||||
}
|
||||
}).parse(html);
|
||||
|
||||
return writer.getContent();
|
||||
};
|
||||
|
||||
export default {
|
||||
cleanHtml,
|
||||
normalizeHtml
|
||||
};
|
||||
Reference in New Issue
Block a user