init
This commit is contained in:
581
public/tinymce/plugins/searchreplace/plugin.js
Normal file
581
public/tinymce/plugins/searchreplace/plugin.js
Normal file
@ -0,0 +1,581 @@
|
||||
/**
|
||||
* plugin.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/*jshint smarttabs:true, undef:true, unused:true, latedef:true, curly:true, bitwise:true */
|
||||
/*global tinymce:true */
|
||||
|
||||
(function() {
|
||||
// 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) {
|
||||
var m, matches = [], text, count = 0, doc;
|
||||
var 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 'findAndReplaceDOMText cannot handle zero-length matches';
|
||||
}
|
||||
|
||||
var index = m.index;
|
||||
|
||||
if (captureGroup > 0) {
|
||||
var cg = m[captureGroup];
|
||||
|
||||
if (!cg) {
|
||||
throw 'Invalid capture group';
|
||||
}
|
||||
|
||||
index += m[0].indexOf(cg);
|
||||
m[0] = cg;
|
||||
}
|
||||
|
||||
return [index, index + m[0].length, [m[0]]];
|
||||
}
|
||||
|
||||
function getText(node) {
|
||||
var txt;
|
||||
|
||||
if (node.nodeType === 3) {
|
||||
return node.data;
|
||||
}
|
||||
|
||||
if (hiddenTextElementsMap[node.nodeName] && !blockElementsMap[node.nodeName]) {
|
||||
return '';
|
||||
}
|
||||
|
||||
txt = '';
|
||||
|
||||
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) {
|
||||
var startNode, endNode, startNodeIndex,
|
||||
endNodeIndex, innerNodes = [], atIndex = 0, curNode = node,
|
||||
matchLocation = matches.shift(), matchIndex = 0;
|
||||
|
||||
out: while (true) {
|
||||
if (blockElementsMap[curNode.nodeName] || shortEndedElementsMap[curNode.nodeName]) {
|
||||
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: startNode,
|
||||
startNodeIndex: startNodeIndex,
|
||||
endNode: endNode,
|
||||
endNodeIndex: endNodeIndex,
|
||||
innerNodes: innerNodes,
|
||||
match: matchLocation[2],
|
||||
matchIndex: 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) {
|
||||
// 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) {
|
||||
var makeReplacementNode;
|
||||
|
||||
if (typeof nodeName != 'function') {
|
||||
var stencilNode = nodeName.nodeType ? nodeName : doc.createElement(nodeName);
|
||||
|
||||
makeReplacementNode = function(fill, matchIndex) {
|
||||
var clone = stencilNode.cloneNode(false);
|
||||
|
||||
clone.setAttribute('data-mce-index', matchIndex);
|
||||
|
||||
if (fill) {
|
||||
clone.appendChild(doc.createTextNode(fill));
|
||||
}
|
||||
|
||||
return clone;
|
||||
};
|
||||
} else {
|
||||
makeReplacementNode = nodeName;
|
||||
}
|
||||
|
||||
return function replace(range) {
|
||||
var before, after, parentNode, startNode = range.startNode,
|
||||
endNode = range.endNode, matchIndex = range.matchIndex;
|
||||
|
||||
if (startNode === endNode) {
|
||||
var 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:
|
||||
var 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;
|
||||
} else {
|
||||
// Replace startNode -> [innerNodes...] -> endNode (in that order)
|
||||
before = doc.createTextNode(startNode.data.substring(0, range.startNodeIndex));
|
||||
after = doc.createTextNode(endNode.data.substring(range.endNodeIndex));
|
||||
var elA = makeReplacementNode(startNode.data.substring(range.startNodeIndex), matchIndex);
|
||||
var innerEls = [];
|
||||
|
||||
for (var i = 0, l = range.innerNodes.length; i < l; ++i) {
|
||||
var innerNode = range.innerNodes[i];
|
||||
var innerEl = makeReplacementNode(innerNode.data, matchIndex);
|
||||
innerNode.parentNode.replaceChild(innerEl, innerNode);
|
||||
innerEls.push(innerEl);
|
||||
}
|
||||
|
||||
var 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;
|
||||
}
|
||||
|
||||
function Plugin(editor) {
|
||||
var self = this, currentIndex = -1;
|
||||
|
||||
function showDialog() {
|
||||
var last = {};
|
||||
|
||||
function updateButtonStates() {
|
||||
win.statusbar.find('#next').disabled(!findSpansByIndex(currentIndex + 1).length);
|
||||
win.statusbar.find('#prev').disabled(!findSpansByIndex(currentIndex - 1).length);
|
||||
}
|
||||
|
||||
function notFoundAlert() {
|
||||
tinymce.ui.MessageBox.alert('Could not find the specified string.', function() {
|
||||
win.find('#find')[0].focus();
|
||||
});
|
||||
}
|
||||
|
||||
var win = tinymce.ui.Factory.create({
|
||||
type: 'window',
|
||||
layout: "flex",
|
||||
pack: "center",
|
||||
align: "center",
|
||||
onClose: function() {
|
||||
editor.focus();
|
||||
self.done();
|
||||
},
|
||||
onSubmit: function(e) {
|
||||
var count, caseState, text, wholeWord;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
caseState = win.find('#case').checked();
|
||||
wholeWord = win.find('#words').checked();
|
||||
|
||||
text = win.find('#find').value();
|
||||
if (!text.length) {
|
||||
self.done(false);
|
||||
win.statusbar.items().slice(1).disabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (last.text == text && last.caseState == caseState && last.wholeWord == wholeWord) {
|
||||
if (findSpansByIndex(currentIndex + 1).length === 0) {
|
||||
notFoundAlert();
|
||||
return;
|
||||
}
|
||||
|
||||
self.next();
|
||||
updateButtonStates();
|
||||
return;
|
||||
}
|
||||
|
||||
count = self.find(text, caseState, wholeWord);
|
||||
if (!count) {
|
||||
notFoundAlert();
|
||||
}
|
||||
|
||||
win.statusbar.items().slice(1).disabled(count === 0);
|
||||
updateButtonStates();
|
||||
|
||||
last = {
|
||||
text: text,
|
||||
caseState: caseState,
|
||||
wholeWord: wholeWord
|
||||
};
|
||||
},
|
||||
buttons: [
|
||||
{text: "Find", onclick: function() {
|
||||
win.submit();
|
||||
}},
|
||||
{text: "Replace", disabled: true, onclick: function() {
|
||||
if (!self.replace(win.find('#replace').value())) {
|
||||
win.statusbar.items().slice(1).disabled(true);
|
||||
currentIndex = -1;
|
||||
last = {};
|
||||
}
|
||||
}},
|
||||
{text: "Replace all", disabled: true, onclick: function() {
|
||||
self.replace(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: function() {
|
||||
self.prev();
|
||||
updateButtonStates();
|
||||
}},
|
||||
{text: "Next", name: 'next', disabled: true, onclick: function() {
|
||||
self.next();
|
||||
updateButtonStates();
|
||||
}}
|
||||
],
|
||||
title: "Find and replace",
|
||||
items: {
|
||||
type: "form",
|
||||
padding: 20,
|
||||
labelGap: 30,
|
||||
spacing: 10,
|
||||
items: [
|
||||
{type: 'textbox', name: 'find', size: 40, label: 'Find', value: editor.selection.getNode().src},
|
||||
{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: ' '}
|
||||
]
|
||||
}
|
||||
}).renderTo().reflow();
|
||||
}
|
||||
|
||||
self.init = function(ed) {
|
||||
ed.addMenuItem('searchreplace', {
|
||||
text: 'Find and replace',
|
||||
shortcut: 'Ctrl+F',
|
||||
onclick: showDialog,
|
||||
separator: 'before',
|
||||
context: 'edit'
|
||||
});
|
||||
|
||||
ed.addButton('searchreplace', {
|
||||
tooltip: 'Find and replace',
|
||||
shortcut: 'Ctrl+F',
|
||||
onclick: showDialog
|
||||
});
|
||||
|
||||
ed.addCommand("SearchReplace", showDialog);
|
||||
|
||||
ed.shortcuts.add('Ctrl+F', '', showDialog);
|
||||
};
|
||||
|
||||
function markAllMatches(regex) {
|
||||
var node, marker;
|
||||
|
||||
marker = editor.dom.create('span', {
|
||||
"class": 'mce-match-marker',
|
||||
"data-mce-bogus": 1
|
||||
});
|
||||
|
||||
node = editor.getBody();
|
||||
|
||||
self.done(false);
|
||||
|
||||
return findAndReplaceDOMText(regex, node, marker, false, editor.schema);
|
||||
}
|
||||
|
||||
function unwrap(node) {
|
||||
var parentNode = node.parentNode;
|
||||
parentNode.insertBefore(node.firstChild, node);
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
|
||||
function findSpansByIndex(index) {
|
||||
var nodes, spans = [];
|
||||
|
||||
nodes = tinymce.toArray(editor.getBody().getElementsByTagName('span'));
|
||||
if (nodes.length) {
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var nodeIndex = nodes[i].getAttribute('data-mce-index');
|
||||
|
||||
if (nodeIndex === null || !nodeIndex.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nodeIndex === index.toString()) {
|
||||
spans.push(nodes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
function moveSelection(forward) {
|
||||
var testIndex = currentIndex, dom = editor.dom;
|
||||
|
||||
forward = forward !== false;
|
||||
|
||||
if (forward) {
|
||||
testIndex++;
|
||||
} else {
|
||||
testIndex--;
|
||||
}
|
||||
|
||||
dom.removeClass(findSpansByIndex(currentIndex), 'mce-match-marker-selected');
|
||||
|
||||
var spans = findSpansByIndex(testIndex);
|
||||
if (spans.length) {
|
||||
dom.addClass(findSpansByIndex(testIndex), 'mce-match-marker-selected');
|
||||
editor.selection.scrollIntoView(spans[0]); // 移动到这个文字, 神奇
|
||||
log(editor)
|
||||
return testIndex;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
function removeNode(node) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
|
||||
self.find = function(text, matchCase, wholeWord) {
|
||||
text = text.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
|
||||
text = wholeWord ? '\\b' + text + '\\b' : text;
|
||||
|
||||
var count = markAllMatches(new RegExp(text, matchCase ? 'g' : 'gi'));
|
||||
|
||||
if (count) {
|
||||
currentIndex = -1;
|
||||
currentIndex = moveSelection(true);
|
||||
}
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
self.next = function() {
|
||||
var index = moveSelection(true);
|
||||
|
||||
if (index !== -1) {
|
||||
currentIndex = index;
|
||||
}
|
||||
};
|
||||
|
||||
self.prev = function() {
|
||||
var index = moveSelection(false);
|
||||
|
||||
if (index !== -1) {
|
||||
currentIndex = index;
|
||||
}
|
||||
};
|
||||
|
||||
self.replace = function(text, forward, all) {
|
||||
var i, nodes, node, matchIndex, currentMatchIndex, nextIndex = currentIndex, hasMore;
|
||||
|
||||
forward = forward !== false;
|
||||
|
||||
node = editor.getBody();
|
||||
nodes = tinymce.toArray(node.getElementsByTagName('span'));
|
||||
for (i = 0; i < nodes.length; i++) {
|
||||
var nodeIndex = nodes[i].getAttribute('data-mce-index');
|
||||
|
||||
if (nodeIndex === null || !nodeIndex.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
matchIndex = currentMatchIndex = parseInt(nodeIndex, 10);
|
||||
if (all || matchIndex === currentIndex) {
|
||||
if (text.length) {
|
||||
nodes[i].firstChild.nodeValue = text;
|
||||
unwrap(nodes[i]);
|
||||
} else {
|
||||
removeNode(nodes[i]);
|
||||
}
|
||||
|
||||
while (nodes[++i]) {
|
||||
matchIndex = nodes[i].getAttribute('data-mce-index');
|
||||
|
||||
if (nodeIndex === null || !nodeIndex.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (matchIndex === currentMatchIndex) {
|
||||
removeNode(nodes[i]);
|
||||
} else {
|
||||
i--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (forward) {
|
||||
nextIndex--;
|
||||
}
|
||||
} else if (currentMatchIndex > currentIndex) {
|
||||
nodes[i].setAttribute('data-mce-index', currentMatchIndex - 1);
|
||||
}
|
||||
}
|
||||
|
||||
editor.undoManager.add();
|
||||
currentIndex = nextIndex;
|
||||
|
||||
if (forward) {
|
||||
hasMore = findSpansByIndex(nextIndex + 1).length > 0;
|
||||
self.next();
|
||||
} else {
|
||||
hasMore = findSpansByIndex(nextIndex - 1).length > 0;
|
||||
self.prev();
|
||||
}
|
||||
|
||||
return !all && hasMore;
|
||||
};
|
||||
|
||||
self.done = function(keepEditorSelection) {
|
||||
var i, nodes, startContainer, endContainer;
|
||||
|
||||
nodes = tinymce.toArray(editor.getBody().getElementsByTagName('span'));
|
||||
for (i = 0; i < nodes.length; i++) {
|
||||
var nodeIndex = nodes[i].getAttribute('data-mce-index');
|
||||
|
||||
if (nodeIndex !== null && nodeIndex.length) {
|
||||
if (nodeIndex === currentIndex.toString()) {
|
||||
if (!startContainer) {
|
||||
startContainer = nodes[i].firstChild;
|
||||
}
|
||||
|
||||
endContainer = nodes[i].firstChild;
|
||||
}
|
||||
|
||||
unwrap(nodes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (startContainer && endContainer) {
|
||||
var rng = editor.dom.createRng();
|
||||
rng.setStart(startContainer, 0);
|
||||
rng.setEnd(endContainer, endContainer.data.length);
|
||||
|
||||
if (keepEditorSelection !== false) {
|
||||
editor.selection.setRng(rng);
|
||||
}
|
||||
|
||||
return rng;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
tinymce.PluginManager.add('searchreplace', Plugin);
|
||||
})();
|
Reference in New Issue
Block a user