1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-09 16:26:59 +02:00

InputfieldTinyMCE: Add support for new "remove all style attributes" option to the Markup Toggle settings. Plus refactoring of the pasteFilter JS in attempt to fix processwire/processwire-issues#1866 which should improve pasting from MS Word.

This commit is contained in:
Ryan Cramer
2024-01-26 13:45:08 -05:00
parent d37b2d40d7
commit 95e10b89b9
4 changed files with 66 additions and 29 deletions

View File

@@ -598,7 +598,7 @@ var InputfieldTinyMCE = {
var t = InputfieldTinyMCE;
var allow = ',' + ProcessWire.config.InputfieldTinyMCE.pasteFilter + ',';
var regexTag = /<([a-z0-9]+)([^>]*)>/gi;
var regexTag = /<([a-z0-9:!\[\]]+)([^>]*)>/gi;
var regexAttr = /([-_a-z0-9]+)=["']([^"']*)["']/gi;
var html = args.content;
var matchTag, matchAttr;
@@ -607,13 +607,30 @@ var InputfieldTinyMCE = {
var replaces = [];
var startLength = html.length;
allow = allow.toLowerCase();
/*
* HTML for testing MS word paste
msWordHtml =
"<p className=MsoNormal>This is <b>bold</b> text. <o:p></o:p></p>\n\n" +
"<h2>This is headline 2. <o:p></o:p></h2>\n\n" +
"<p className=MsoNormal>This is <I>italic</I> text<o:p></o:p></p>\n\n" +
"<p className=MsoListParagraphCxSpFirst style='text-indent:-.25in;mso-list:10 level1 lfo1'>" +
"<![if !supportsLists]><span style='iso-bidi-font-family:Aptos;mso-bidi-theme-font:minor-latin'>" +
"<span style='so-list:Ignore'>1.<span style='font:7.0pt \"Times New Roman\"'>" +
"&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></span></span><![endif]>One <o:p></o:p></p>";
html = msWordHtml;
*/
startLength = html.length;
if(args.internal) {
t.log('Skipping pasteFilter for interal copy/paste');
return; // skip filtering for internal copy/paste operations
}
if(allow === ',text,') {
t.log('Skipping pasteFilter since paste_as_text settingw will be used');
t.log('Skipping pasteFilter since paste_as_text settings will be used');
return; // will be processed by paste_as_text setting
}
@@ -621,17 +638,19 @@ var InputfieldTinyMCE = {
var tagOpen = matchTag[0]; // i.e. <strong>, <img src="..">, <h2>, etc.
var tagName = matchTag[1]; // i.e. 'strong', 'img', 'h2', etc.
var tagNameLower = tagName.toLowerCase();
var tagClose = '</' + tagName + '>'; // i.e. </strong>, </h2>
var tagAttrs = matchTag[2]; // i.e. 'src="a.jpg" alt="alt"'
var allowAttrs = false;
// first see if we can match a tag replacement
var find = ',' + tagName + '='; // i.e. ',b=strong'
var pos = allow.indexOf(find);
var findTagEqual = ',' + tagNameLower + '='; // i.e. ',b=strong'
var findTagAttr = ',' + tagNameLower + '['; // i.e. ',b[...]'
var findTagOnly = ',' + tagNameLower + ','; // i.e. ,b
var posTagEqual = allow.indexOf(findTagEqual);
if(pos > -1) {
// tag replacement
var rule = allow.substring(pos + 1); // i.e. b=strong,and,more
if(posTagEqual > -1) {
var rule = allow.substring(posTagEqual + 1); // i.e. b=strong,and,more
rule = rule.substring(0, rule.indexOf(',')); // i.e. b=strong
rule = rule.split('=');
var replaceTag = rule[1];
@@ -640,12 +659,12 @@ var InputfieldTinyMCE = {
finds.push(tagClose);
replaces.push('</' + replaceTag + '>');
}
if(allow.indexOf(',' + tagName + '[') > -1) {
if(allow.indexOf(findTagAttr) > -1) {
// tag appears in whitelist with attributes
allowAttrs = true;
} else if(allow.indexOf(',' + tagName + ',') === -1) {
// tag does not appear in whitelist
} else if(posTagEqual === -1 && allow.indexOf(findTagOnly) === -1) {
// tag does not appear in whitelist at all
removals.push(tagOpen);
removals.push(tagClose);
continue;

View File

@@ -74,7 +74,7 @@ class InputfieldTinyMCE extends InputfieldTextarea implements ConfigurableModule
return array(
'title' => 'TinyMCE',
'summary' => 'TinyMCE rich text editor version ' . self::mceVersion . '.',
'version' => 617,
'version' => 618,
'icon' => 'keyboard-o',
'requires' => 'ProcessWire>=3.0.200, MarkupHTMLPurifier',
);
@@ -89,6 +89,7 @@ class InputfieldTinyMCE extends InputfieldTextarea implements ConfigurableModule
const toggleCleanDiv = 2; // remove <div>s
const toggleCleanP = 4; // remove empty <p> tags
const toggleCleanNbsp = 8; // remove &nbsp; entities
const toggleRemoveStyles = 16; // remove all style attributes
/**
* Default configuration for filtered paste

View File

@@ -711,6 +711,7 @@ class InputfieldTinyMCEConfigs extends InputfieldTinyMCEClass {
$f->addOption(InputfieldTinyMCE::toggleCleanDiv, $this->_('Convert `<div>` tags to `<p>` tags on save?'));
$f->addOption(InputfieldTinyMCE::toggleCleanP, $this->_('Remove empty `<p>` tags on save?'));
$f->addOption(InputfieldTinyMCE::toggleCleanNbsp, $this->_('Remove non-breaking spaces on save?'));
$f->addOption(InputfieldTinyMCE::toggleRemoveStyles, $this->_('Remove `style` attributes from all elements'));
$f->attr('value', $this->inputfield->toggles);
$f->collapsed = Inputfield::collapsedYes;
$f->themeOffset = 1;
@@ -1385,4 +1386,4 @@ class InputfieldTinyMCEConfigs extends InputfieldTinyMCEClass {
'usePurifier',
);
}
}

View File

@@ -162,25 +162,41 @@ class InputfieldTinyMCETools extends InputfieldTinyMCEClass {
// convert <div> to paragraphs
$toggles = $this->inputfield->toggles;
if(!is_array($toggles)) return $value;
if(in_array(InputfieldTinyMCE::toggleCleanDiv, $toggles) && strpos($value, '<div') !== false) {
$value = preg_replace('{\s*(</?)div[^><]*>\s*}is', '$1' . 'p>', $value);
while(strpos($value, '<p><p>') !== false) {
$value = str_replace(array('<p><p>', '</p></p>'), array('<p>', '</p>'), $value);
foreach($toggles as $toggle) {
switch($toggle) {
case InputfieldTinyMCE::toggleCleanDiv:
// convert <div> to <p>
if(strpos($value, '<div') !== false) {
$value = preg_replace('{\s*(</?)div[^><]*>\s*}is', '$1' . 'p>', $value);
while(strpos($value, '<p><p>') !== false) {
$value = str_replace(array('<p><p>', '</p></p>'), array('<p>', '</p>'), $value);
}
}
break;
case InputfieldTinyMCE::toggleCleanP:
// remove empty paragraphs
$value = str_replace(array('<p><br /></p>', '<p>&nbsp;</p>', "<p>\xc2\xa0</p>", '<p></p>', '<p> </p>'), '', $value);
break;
case InputfieldTinyMCE::toggleCleanNbsp:
// convert non-breaking space to regular space
$value = str_ireplace('&nbsp;', ' ', $value);
$value = str_replace("\xc2\xa0",' ', $value);
break;
case InputfieldTinyMCE::toggleRemoveStyles:
// remove all style attributes
if(strpos($value, 'style=')) {
if(preg_match_all('!(<.+?)\sstyle=(["\']).*?\2!i', $value, $matches)) {
foreach($matches[0] as $key => $fullMatch) {
$startMatch = $matches[1][$key];
$value = str_replace($fullMatch, $startMatch, $value);
}
}
}
break;
}
}
// remove gratuitous whitespace
if(in_array(InputfieldTinyMCE::toggleCleanP, $toggles)) {
$value = str_replace(array('<p><br /></p>', '<p>&nbsp;</p>', "<p>\xc2\xa0</p>", '<p></p>', '<p> </p>'), '', $value);
}
// convert non-breaking space to regular space
if(in_array(InputfieldTinyMCE::toggleCleanNbsp, $toggles)) {
$value = str_ireplace('&nbsp;', ' ', $value);
$value = str_replace("\xc2\xa0",' ', $value);
}
return $value;
}