diff --git a/CHANGELOG b/CHANGELOG index 10394a7bd..f495f2717 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -34,6 +34,8 @@ Motions: - Added config value for customize sorting of category list in pdf/docx export [#3329]. - Added config value for pagenumber alignment in PDF [#3327]. +- Bugfix: Several bugfixes regarding splitting list items in + change recommendations [#3288] Users: - User without permission to see users can now see agenda item speakers, diff --git a/openslides/core/static/css/app.css b/openslides/core/static/css/app.css index da95f4e76..dedd997c6 100644 --- a/openslides/core/static/css/app.css +++ b/openslides/core/static/css/app.css @@ -109,6 +109,18 @@ h4 { p { padding-bottom: 14px; } +p.os-split-after { + padding-bottom: 0; + margin-bottom: 0; +} +p.os-split-before { + padding-top: 0; + margin-top: 0; +} + +ul.os-split-after, ol.os-split-after { + margin-bottom: 0; +} a { text-decoration: none; @@ -607,6 +619,10 @@ img { /** Diff view */ +.motion-text-holder li.os-split-before { + list-style-type: none; +} + .change-recommendation-overview { background-color: #eee; border: solid 1px #ddd; @@ -657,19 +673,24 @@ img { border: solid 1px #eee; border-radius: 3px; margin-bottom: 0; - margin-top: -24px; padding-top: 0; padding-right: 155px; } .diff-box-transparent { margin-top: -24px; } +.motion-text-with-diffs li.os-split-before { + list-style-type: none; +} .motion-text-with-diffs .original-text { min-height: 30px; /* Spacer between .diff-box, in case .original-text is empty */ } -.motion-text-with-diffs .original-text ul:last-child { +.motion-text-with-diffs .original-text ul:last-child, .motion-text-with-diffs .original-text ol:last-child { padding-bottom: 16px; } +.motion-text-with-diffs .original-text ul.os-split-after:last-child, .motion-text-with-diffs .original-text ol.os-split-after:last-child { + padding-bottom: 0; +} .motion-text-with-diffs.line-numbers-inline .diff-box, .motion-text-with-diffs.line-numbers-none .diff-box { margin-right: -220px; } @@ -713,9 +734,6 @@ img { text-decoration: underline; } .motion-text-diff p { - padding-bottom: 0; - margin-top: 0; - margin-bottom: 0; } .motion-text-diff.line-numbers-outside .insert .os-line-number { display: none; diff --git a/openslides/core/static/css/projector.css b/openslides/core/static/css/projector.css index ef5d7ec04..b4fbf4531 100644 --- a/openslides/core/static/css/projector.css +++ b/openslides/core/static/css/projector.css @@ -538,7 +538,9 @@ tr.elected td { .diff-box { margin-left: 25px; padding-top: 0; - margin-top: -10px; +} +.motion-text-with-diffs li.os-split-before { + list-style-type: none; } .motion-text-with-diffs .original-text { min-height: 30px; // Spacer between .diff-box, in case .original-text is empty @@ -546,6 +548,16 @@ tr.elected td { .motion-text-with-diffs .original-text ul:last-child { padding-bottom: 16px; } +.motion-text-with-diffs .original-text ul.os-split-after:last-child { + padding-bottom: 0; +} +ol.os-split-after, ul.os-split-after { + margin-bottom: 0; + padding-bottom: 0; +} +p.os-split-after { + margin-bottom: 0; +} .motion-text-with-diffs.line-numbers-inline .diff-box, .motion-text-with-diffs.line-numbers-none .diff-box { margin-right: -220px; } @@ -560,8 +572,11 @@ tr.elected td { color: green; text-decoration: underline; } -.motion-text-diff p { - padding-bottom: 0; +.motion-text-diff p.os-split-before { + padding-top: 0; + margin-top: 0; +} +.motion-text-diff p.os-split-after { margin-top: 0; margin-bottom: 0; } diff --git a/openslides/core/static/js/core/base.js b/openslides/core/static/js/core/base.js index 56c93d570..152646efd 100644 --- a/openslides/core/static/js/core/base.js +++ b/openslides/core/static/js/core/base.js @@ -920,13 +920,13 @@ angular.module('OpenSlidesApp.core', [ allowedContent: 'h1 h2 h3 b i u strike sup sub strong em;' + 'blockquote p pre table' + - '(text-align-left,text-align-center,text-align-right,text-align-justify){text-align};' + + '(text-align-left,text-align-center,text-align-right,text-align-justify,os-split-before,os-split-after){text-align};' + 'a[!href];' + 'img[!src,alt]{width,height,float};' + 'tr th td caption;' + - 'li; ol[start]{list-style-type};' + - 'ul{list-style};' + - 'span[!*]{color,background-color}(os-line-number,line-number-*);' + + 'li(os-split-before,os-split-after); ol(os-split-before,os-split-after)[start]{list-style-type};' + + 'ul(os-split-before,os-split-after){list-style};' + + 'span[!*]{color,background-color}(os-split-before,os-split-after,os-line-number,line-number-*);' + 'br(os-line-break);', // there seems to be an error in CKeditor that parses spaces in extraPlugins as part of the plugin name. diff --git a/openslides/motions/static/js/motions/diff.js b/openslides/motions/static/js/motions/diff.js index 208b53725..920ec857a 100644 --- a/openslides/motions/static/js/motions/diff.js +++ b/openslides/motions/static/js/motions/diff.js @@ -308,6 +308,25 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi return fragment; }; + /** + * When a
Line 1
Line 2
Line 3
Line 1
+ * - extracting line 2 to 3 results inLine 2
+ * - extracting line 3 to null/4 results inLine 3
*/ - this.extractRangeByLineNumbers = function(htmlIn, fromLine, toLine, debug) { + this.extractRangeByLineNumbers = function(htmlIn, fromLine, toLine) { if (typeof(htmlIn) !== 'string') { throw 'Invalid call - extractRangeByLineNumbers expects a string as first argument'; } @@ -384,27 +413,51 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi toChildTraceAbs.shift(); var followingHtml = this._serializePartialDomFromChild(fragment, toChildTraceAbs, false); - var currNode = fromLineNode.parentNode; + var currNode = fromLineNode, + isSplit = false; while (currNode.parentNode) { - previousHtmlEndSnippet += '' + currNode.nodeName + '>'; + if (!this._isFirstNonemptyChild(currNode.parentNode, currNode)) { + isSplit = true; + } + if (isSplit) { + this.addCSSClass(currNode.parentNode, 'os-split-before'); + } + if (currNode.nodeName !== 'OS-LINEBREAK') { + previousHtmlEndSnippet += '' + currNode.nodeName + '>'; + } currNode = currNode.parentNode; } - currNode = toLineNode.parentNode; + + currNode = toLineNode; + isSplit = false; while (currNode.parentNode) { - followingHtmlStartSnippet = this._serializeTag(currNode) + followingHtmlStartSnippet; + if (!this._isFirstNonemptyChild(currNode.parentNode, currNode)) { + isSplit = true; + } + if (isSplit) { + this.addCSSClass(currNode.parentNode, 'os-split-after'); + } + followingHtmlStartSnippet = this._serializeTag(currNode.parentNode) + followingHtmlStartSnippet; currNode = currNode.parentNode; } var found = false; + isSplit = false; for (var i = 0; i < fromChildTraceRel.length && !found; i++) { if (fromChildTraceRel[i].nodeName === 'OS-LINEBREAK') { found = true; } else { + if (!this._isFirstNonemptyChild(fromChildTraceRel[i], fromChildTraceRel[i + 1])) { + isSplit = true; + } if (fromChildTraceRel[i].nodeName === 'OL') { fakeOl = fromChildTraceRel[i].cloneNode(false); fakeOl.setAttribute('start', this._isWithinNthLIOfOL(fromChildTraceRel[i], fromLineNode)); innerContextStart += this._serializeTag(fakeOl); } else { + if (i < (fromChildTraceRel.length - 1) && isSplit) { + this.addCSSClass(fromChildTraceRel[i], 'os-split-before'); + } innerContextStart += this._serializeTag(fromChildTraceRel[i]); } } @@ -512,28 +565,28 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi var lastNode = nodes1[nodes1.length - 1], firstNode = nodes2[0]; - if (lastNode.nodeType == TEXT_NODE && firstNode.nodeType == TEXT_NODE) { + if (lastNode.nodeType === TEXT_NODE && firstNode.nodeType === TEXT_NODE) { var newTextNode = lastNode.ownerDocument.createTextNode(lastNode.nodeValue + firstNode.nodeValue); out.push(newTextNode); - } else if (lastNode.nodeName == firstNode.nodeName) { + } else if (lastNode.nodeName === firstNode.nodeName) { var newNode = lastNode.ownerDocument.createElement(lastNode.nodeName); for (i = 0; i < lastNode.attributes.length; i++) { var attr = lastNode.attributes[i]; newNode.setAttribute(attr.name, attr.value); } - // Remove #text nodes inside of List elements, as they are confusing + // Remove #text nodes inside of List elements (OL/UL), as they are confusing var lastChildren, firstChildren; - if (lastNode.nodeName == 'OL' || lastNode.nodeName == 'UL') { + if (lastNode.nodeName === 'OL' || lastNode.nodeName === 'UL') { lastChildren = []; firstChildren = []; for (i = 0; i < firstNode.childNodes.length; i++) { - if (firstNode.childNodes[i].nodeType == ELEMENT_NODE) { + if (firstNode.childNodes[i].nodeType === ELEMENT_NODE) { firstChildren.push(firstNode.childNodes[i]); } } for (i = 0; i < lastNode.childNodes.length; i++) { - if (lastNode.childNodes[i].nodeType == ELEMENT_NODE) { + if (lastNode.childNodes[i].nodeType === ELEMENT_NODE) { lastChildren.push(lastNode.childNodes[i]); } } @@ -546,12 +599,13 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi for (i = 0; i < children.length; i++) { newNode.appendChild(children[i]); } + out.push(newNode); } else { - if (lastNode.nodeName != 'TEMPLATE') { + if (lastNode.nodeName !== 'TEMPLATE') { out.push(lastNode); } - if (firstNode.nodeName != 'TEMPLATE') { + if (firstNode.nodeName !== 'TEMPLATE') { out.push(firstNode); } } @@ -570,9 +624,23 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi * @private */ this._normalizeHtmlForDiff = function (html) { - // Convert all HTML tags to uppercase, strip trailing whitespaces - html = html.replace(/<[^>]+>/g, function (tag) { - return tag.toUpperCase(); + // Convert all HTML tags to uppercase, but leave the values of attributes unchanged + html = html.replace(/<(\/?[a-z]*)( [^>]*)?>/ig, function (html, tag, attributes) { + var tagNormalized = tag.toUpperCase(); + if (attributes === undefined) { + attributes = ""; + } + attributes = attributes.replace(/( [^"'=]*)(= *((["'])(.*?)\4))?/gi, function (attr, attrName, attrRest, attrRest2, quot, attrValue) { + var attrNormalized = attrName.toUpperCase(); + if (attrRest !== undefined) { + if (attrNormalized === ' CLASS') { + attrValue = attrValue.split(' ').sort().join(' '); + } + attrNormalized += "=" + quot + attrValue + quot; + } + return attrNormalized; + }); + return "<" + tagNormalized + attributes + ">"; }); var entities = { @@ -649,7 +717,7 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi followingFragment = this.htmlToFragment(followingHtml), newFragment = this.htmlToFragment(newHTML); - if (data.html.length > 0 && data.html.substr(-1) == ' ') { + if (data.html.length > 0 && data.html.substr(-1) === ' ') { this._insertDanglingSpace(newFragment); } @@ -667,21 +735,46 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi el.parentNode.removeChild(el); } + var forgottenSplitClasses = mergedFragment.querySelectorAll(".os-split-before, .os-split-after"); + for (i = 0; i < forgottenSplitClasses.length; i++) { + this.removeCSSClass(forgottenSplitClasses[i], 'os-split-before'); + this.removeCSSClass(forgottenSplitClasses[i], 'os-split-after'); + } + return this._serializeDom(mergedFragment, true); }; this.addCSSClass = function (node, className) { - if (node.nodeType != ELEMENT_NODE) { + if (node.nodeType !== ELEMENT_NODE) { return; } var classes = node.getAttribute('class'); classes = (classes ? classes.split(' ') : []); - if (classes.indexOf(className) == -1) { + if (classes.indexOf(className) === -1) { classes.push(className); } node.setAttribute('class', classes.join(' ')); }; + this.removeCSSClass = function (node, className) { + if (node.nodeType !== ELEMENT_NODE) { + return; + } + var classes = node.getAttribute('class'), + newClasses = []; + classes = (classes ? classes.split(' ') : []); + for (var i = 0; i < classes.length; i++) { + if (classes[i] !== className) { + newClasses.push(classes[i]); + } + } + if (newClasses.length === 0) { + node.removeAttribute('class'); + } else { + node.setAttribute('class', newClasses.join(' ')); + } + }; + this.addDiffMarkup = function (originalHTML, newHTML, fromLine, toLine, diffFormatterCb) { var data = this.extractRangeByLineNumbers(originalHTML, fromLine, toLine), previousHtml = data.previousHtml + '' + data.previousHtmlEndSnippet, @@ -1151,19 +1244,14 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi // RemoveLine 1 '); + expect(diff.html).toBe('
Line 1 '); expect(diff.outerContextStart).toBe(''); expect(diff.outerContextEnd).toBe(''); }); it('extracts lines from nested UL/LI-structures', function () { var diff = diffService.extractRangeByLineNumbers(baseHtml1, 7, 9); - expect(diff.html).toBe('Line 7
' + noMarkup(1) + 'Line 1
' + + '' + noMarkup(2) + 'Line 2' + brMarkup(3) + 'Line 3' + brMarkup(4) + 'Line 5
'); + expect(diff.outerContextEnd).toBe('
'); + }); + + it('extracts lines from double-nested UL/LI-structures (2)', function () { + var html = '
' + noMarkup(1) + 'Line 1
' + + '' + noMarkup(2) + 'Line 2' + brMarkup(3) + 'Line 3' + brMarkup(4) + '
Line 2'); + expect(diff.outerContextStart).toBe(''); + expect(diff.outerContextEnd).toBe(''); + expect(diff.innerContextStart).toBe(''); + expect(diff.innerContextEnd).toBe('
'); }); it('extracts a single line right before a UL/LI', function () { @@ -155,14 +186,14 @@ describe('linenumbering', function () { it('extracts lines from a more complex example', function () { var diff = diffService.extractRangeByLineNumbers(baseHtml2, 6, 11); - expect(diff.html).toBe('owe. Dahoam gscheckate middn Spuiratz des is a gmahde Wiesn. Des is schee so Obazda san da, Haferl pfenningguat schoo griasd eich midnand.
'); + expect(diff.innerContextStart).toBe('
'); expect(diff.innerContextEnd).toBe('
'); + expect(diff.innerContextStart).toBe('
'); expect(diff.innerContextEnd).toBe(''); expect(diff.previousHtmlEndSnippet).toBe('
'); expect(diff.followingHtml).toBe(''); @@ -184,7 +215,7 @@ describe('linenumbering', function () { expect(diff.html).toBe('Test
", + normalized = diffService._normalizeHtmlForDiff(unnormalized); + expect(normalized).toBe("Test
"); + }); + }); + + describe('the core diff algorithm', function () { it('acts as documented by the official documentation', function () { var before = "The red brown fox jumped over the rolling log.", after = "The brown spotted fox leaped over the rolling log."; @@ -529,11 +611,11 @@ describe('linenumbering', function () { }); it('detects broken HTML and lowercases class names', function () { - var before = "holen, da rief sie alle sieben herbei und sprach:
\n\n \"Liebe Kinder, ich will hinaus in den Wald, seid auf der Hut vor dem Wolf! Wenn er
hereinkommt, frisst er euch alle mit Haut und Haar. Der Bösewicht verstellt sich oft, aber
an der rauen Stimme und an seinen schwarzen Füßen werdet ihr ihn schon erkennen.\"
Die Geißlein sagten: \" Liebe Mutter, wir wollen uns schon in acht nehmen, du kannst ohne
", + var before = "holen, da rief sie alle sieben herbei und sprach:
\n\n \"Liebe Kinder, ich will hinaus in den Wald, seid auf der Hut vor dem Wolf! Wenn er
hereinkommt, frisst er euch alle mit Haut und Haar. Der Bösewicht verstellt sich oft, aber
an der rauen Stimme und an seinen schwarzen Füßen werdet ihr ihn schon erkennen.\"
Die Geißlein sagten: \" Liebe Mutter, wir wollen uns schon in acht nehmen, du kannst ohne
", after = "holen, da rief sie alle sieben herbei und sprach:
\n\nHello
\n\nWorld
\n\nYa
\n\nDie Geißlein sagten: \" Liebe Mutter, wir wollen uns schon in acht nehmen, du kannst ohne
"; var diff = diffService.diff(before, after); - expect(diff).toBe("holen, da rief sie alle sieben herbei und sprach:
\"Liebe Kinder, ich will hinaus in den Wald, seid auf der Hut vor dem Wolf! Wenn er
hereinkommt, frisst er euch alle mit Haut und Haar. Der Bösewicht verstellt sich oft, aber
an der rauen Stimme und an seinen schwarzen Füßen werdet ihr ihn schon erkennen.\"
Die Geißlein sagten: \" Liebe Mutter, wir wollen uns schon in acht nehmen, du kannst ohne
" + + expect(diff).toBe("holen, da rief sie alle sieben herbei und sprach:
\"Liebe Kinder, ich will hinaus in den Wald, seid auf der Hut vor dem Wolf! Wenn er
hereinkommt, frisst er euch alle mit Haut und Haar. Der Bösewicht verstellt sich oft, aber
an der rauen Stimme und an seinen schwarzen Füßen werdet ihr ihn schon erkennen.\"
Die Geißlein sagten: \" Liebe Mutter, wir wollen uns schon in acht nehmen, du kannst ohne
" + "holen, da rief sie alle sieben herbei und sprach:
\n\n" + "Hello
\n\n" + "World
\n\n" + @@ -554,5 +636,13 @@ describe('linenumbering', function () { inserted = diffService.addCSSClassToFirstTag(strIn, "newClass"); expect(inserted).toBe("