diff --git a/DEVELOPMENT.rst b/DEVELOPMENT.rst
index d54b47776..0e2358029 100644
--- a/DEVELOPMENT.rst
+++ b/DEVELOPMENT.rst
@@ -96,3 +96,12 @@ To setup and activate the virtual environment in step c. use::
> .virtualenv\Scripts\activate.bat
All other commands are the same as for GNU/Linux and Mac OS X.
+
+
+3. Running the test cases
+-------------------------
+
+a. Running Angular.js test cases
+''''''''''''''''''''''''''''''''
+
+ $ node_modules/.bin/karma start tests/karma/karma.conf.js
diff --git a/openslides/core/static/css/app.css b/openslides/core/static/css/app.css
index f6d2974d4..89866095d 100644
--- a/openslides/core/static/css/app.css
+++ b/openslides/core/static/css/app.css
@@ -23,6 +23,10 @@ div {
text-align: left;
}
+blockquote {
+ font-size: inherit;
+}
+
h1, h2, h3, h4, h5, h6 {
font-family: "Roboto Condensed",Helvetica,Arial,sans-serif;
font-weight: 400;
@@ -295,15 +299,138 @@ img {
width: auto;
margin-top: 20px;
background: #fff;
- padding: 20px;
border: 1px solid #d3d3d3;
}
+.col1 .details .line-number-setter {
+ margin-top: 0;
+ margin-bottom: 55px;
+ margin-left: 15px;
+}
+
+.col1 .details .line-number-setter .btn.disabled {
+ cursor: default;
+ opacity: 1;
+ background-color: #eee;
+}
+
+.col1 .details .inline-editing-activator {
+ margin-right: 13px;
+}
+
.col1 ul, .col1 ol {
margin-left: 20px;
}
+/* Toolbar to save motion in inline editing mode */
+.motion-save-toolbar {
+ position: fixed;
+ bottom: 0;
+ left: 50%;
+ height: 75px;
+ width: 300px;
+ background: rgba(242, 222, 222, 0.9);
+ color: black;
+ text-align: center;
+ padding: 5px;
+ z-index: 10;
+ display: none;
+ border: 1px solid #d3d3d3;
+ margin-left: -150px;
+ border-bottom: none;
+ -webkit-box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.75);
+ -moz-box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.75);
+ box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.75);
+}
+.motion-save-toolbar.visible {
+ display: block;
+}
+
+.motion-save-toolbar .changed-hint {
+ display: block;
+ line-height: 16px;
+ text-align: center;
+ margin-bottom: 10px;
+ font-weight: bold;
+}
+
+.motion-save-toolbar label {
+ font-weight: normal;
+ line-height: 16px;
+ text-align: left;
+ padding-left: 16px;
+ margin-top: 5px;
+ margin-left: 15px;
+}
+
+.motion-save-toolbar label input {
+ margin-left: -16px;
+}
+
+/*** Line numbers ***/
+.motion-text.line-numbers-outside {
+ padding-left: 35px;
+ position: relative;
+}
+.motion-text.line-numbers-outside .os-line-number {
+ display: inline-block;
+ font-size: 0;
+ line-height: 0;
+ width: 0;
+ height: 0;
+}
+.motion-text.line-numbers-outside .os-line-number:after {
+ content: attr(data-line-number);
+ position: absolute;
+ left: 0;
+ vertical-align: top;
+ margin-top: -5px;
+ color: gray;
+ font-family: Courier, serif;
+ font-size: 13px;
+}
+
+.motion-text.line-numbers-inline .os-line-break {
+ display: none;
+}
+.motion-text.line-numbers-inline .os-line-number {
+ display: inline-block;
+}
+.motion-text.line-numbers-inline .os-line-number:after {
+ display: inline-block;
+ content: attr(data-line-number);
+ vertical-align: top;
+ font-size: 11px;
+ color: gray;
+ font-family: Courier, serif;
+ margin-top: -3px;
+ margin-left: 0;
+ margin-right: 0;
+}
+
+.motion-text.line-numbers-none .os-line-break {
+ display: none;
+}
+.motion-text.line-numbers-none .os-line-number {
+ display: none;
+}
+
+.os-line-number {
+ user-select: none;
+ -moz-user-select: none;
+ -khtml-user-select: none;
+ -webkit-user-select: none;
+ -o-user-select: none;
+}
+.os-line-number:after {
+ user-select: none;
+ -moz-user-select: none;
+ -khtml-user-select: none;
+ -webkit-user-select: none;
+ -o-user-select: none;
+}
+
/** Projector sidebar column **/
#content .col2 {
diff --git a/openslides/core/static/css/projector.css b/openslides/core/static/css/projector.css
index 63aed0c98..6bb6d300d 100644
--- a/openslides/core/static/css/projector.css
+++ b/openslides/core/static/css/projector.css
@@ -335,3 +335,50 @@ tr.elected td {
.nextSpeakers li {
line-height: 150%;
}
+
+
+
+/*** Line numbers ***/
+.motion-text.line-numbers-outside {
+ padding-left: 0;
+ margin-left: 25px;
+ position: relative;
+}
+
+.motion-text.line-numbers-outside .os-line-number {
+ display: inline-block;
+}
+.motion-text.line-numbers-outside .os-line-number:after {
+ content: attr(data-line-number);
+ position: absolute;
+ left: -25px;
+ vertical-align: top;
+ color: gray;
+ font-family: Courier, serif;
+ font-size: 13px;
+}
+
+.motion-text.line-numbers-inline .os-line-break {
+ display: none;
+}
+.motion-text.line-numbers-inline .os-line-number {
+ display: inline-block;
+}
+.motion-text.line-numbers-inline .os-line-number:after {
+ display: inline-block;
+ content: attr(data-line-number);
+ vertical-align: top;
+ font-size: 0.75em;
+ color: gray;
+ font-family: Courier, serif;
+ margin-top: -5px;
+ margin-left: 0;
+}
+
+
+.motion-text.line-numbers-none .os-line-break {
+ display: none;
+}
+.motion-text.line-numbers-none .os-line-number {
+ display: none;
+}
diff --git a/openslides/core/static/js/core/site.js b/openslides/core/static/js/core/site.js
index 8366bbe6d..77a347a01 100644
--- a/openslides/core/static/js/core/site.js
+++ b/openslides/core/static/js/core/site.js
@@ -768,12 +768,15 @@ angular.module('OpenSlidesApp.core.site', [
'gettextCatalog',
function (gettextCatalog) {
return {
- getOptions: function (images) {
+ getOptions: function (images, inlineMode) {
+ if (inlineMode === undefined) {
+ inlineMode = false;
+ }
return {
language_url: '/static/tinymce/i18n/' + gettextCatalog.getCurrentLanguage() + '.js',
theme_url: '/static/js/openslides-libs.js',
skin_url: '/static/tinymce/skins/lightgray/',
- inline: false,
+ inline: inlineMode,
statusbar: false,
browser_spellcheck: true,
image_advtab: true,
diff --git a/openslides/motions/apps.py b/openslides/motions/apps.py
index 228698ef7..0f08c6445 100644
--- a/openslides/motions/apps.py
+++ b/openslides/motions/apps.py
@@ -7,7 +7,8 @@ class MotionsAppConfig(AppConfig):
verbose_name = 'OpenSlides Motion'
angular_site_module = True
angular_projector_module = True
- js_files = ['js/motions/base.js', 'js/motions/site.js', 'js/motions/projector.js']
+ js_files = ['js/motions/base.js', 'js/motions/site.js', 'js/motions/projector.js',
+ 'js/motions/linenumbering.js', 'js/motions/diff.js']
def ready(self):
# Load projector elements.
diff --git a/openslides/motions/config_variables.py b/openslides/motions/config_variables.py
index 7071e73ce..18d1ac59c 100644
--- a/openslides/motions/config_variables.py
+++ b/openslides/motions/config_variables.py
@@ -56,6 +56,30 @@ def get_config_variables():
subgroup='General',
translatable=True)
+ yield ConfigVariable(
+ name='motions_default_line_numbering',
+ default_value='none',
+ input_type='choice',
+ label='Default line numbering',
+ choices=(
+ {'value': 'outside', 'display_name': 'Outside'},
+ {'value': 'inline', 'display_name': 'Inline'},
+ {'value': 'none', 'display_name': 'None'}),
+ weight=322,
+ group='Motions',
+ subgroup='General')
+
+ yield ConfigVariable(
+ name='motions_line_length',
+ default_value=80,
+ input_type='integer',
+ label='Line length',
+ help_text='The maximum number of characters per line. Relevant when line numbering is enabled. Min: 40',
+ weight=323,
+ group='Motions',
+ subgroup='General',
+ validators=(MinValueValidator(40),))
+
yield ConfigVariable(
name='motions_stop_submitting',
default_value=False,
diff --git a/openslides/motions/static/js/motions/base.js b/openslides/motions/static/js/motions/base.js
index 69d7b8c1d..dc7c302c7 100644
--- a/openslides/motions/static/js/motions/base.js
+++ b/openslides/motions/static/js/motions/base.js
@@ -2,7 +2,11 @@
"use strict";
-angular.module('OpenSlidesApp.motions', ['OpenSlidesApp.users'])
+angular.module('OpenSlidesApp.motions', [
+ 'OpenSlidesApp.users',
+ 'OpenSlidesApp.motions.lineNumbering',
+ 'OpenSlidesApp.motions.diff'
+])
.factory('WorkflowState', [
'DS',
@@ -112,7 +116,8 @@ angular.module('OpenSlidesApp.motions', ['OpenSlidesApp.users'])
'gettext',
'operator',
'Config',
- function(DS, MotionPoll, jsDataModel, gettext, operator, Config) {
+ 'lineNumberingService',
+ function(DS, MotionPoll, jsDataModel, gettext, operator, Config, lineNumberingService) {
var name = 'motions/motion';
return DS.defineResource({
name: name,
@@ -140,6 +145,15 @@ angular.module('OpenSlidesApp.motions', ['OpenSlidesApp.users'])
getText: function (versionId) {
return this.getVersion(versionId).text;
},
+ getTextWithLineBreaks: function (versionId) {
+ var lineLength = Config.get('motions_line_length').value,
+ html = this.getVersion(versionId).text;
+
+ return lineNumberingService.insertLineNumbers(html, lineLength);
+ },
+ setTextStrippingLineBreaks: function (versionId, text) {
+ this.text = lineNumberingService.stripLineNumbers(text);
+ },
getReason: function (versionId) {
return this.getVersion(versionId).reason;
},
diff --git a/openslides/motions/static/js/motions/diff.js b/openslides/motions/static/js/motions/diff.js
new file mode 100644
index 000000000..78cd07a9d
--- /dev/null
+++ b/openslides/motions/static/js/motions/diff.js
@@ -0,0 +1,407 @@
+(function () {
+
+"use strict";
+
+angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumbering'])
+
+.service('diffService', function (lineNumberingService) {
+ var ELEMENT_NODE = 1,
+ TEXT_NODE = 3,
+ DOCUMENT_FRAGMENT_NODE = 11;
+
+
+ this.getLineNumberNode = function(fragment, lineNumber) {
+ return fragment.querySelector('os-linebreak.os-line-number.line-number-' + lineNumber);
+ };
+
+ this._getNodeContextTrace = function(node) {
+ var context = [],
+ currNode = node;
+ while (currNode) {
+ context.unshift(currNode);
+ currNode = currNode.parentNode;
+ }
+ return context;
+ };
+
+ this._insertInternalLineMarkers = function(fragment) {
+ if (fragment.querySelectorAll('OS-LINEBREAK').length > 0) {
+ // Prevent duplicate calls
+ return;
+ }
+ var lineNumbers = fragment.querySelectorAll('span.os-line-number');
+ for (var i = 0; i < lineNumbers.length; i++) {
+ var insertBefore = lineNumbers[i];
+ while (insertBefore.parentNode.nodeType != DOCUMENT_FRAGMENT_NODE && insertBefore.parentNode.childNodes[0] == insertBefore) {
+ insertBefore = insertBefore.parentNode;
+ }
+ var lineMarker = document.createElement('OS-LINEBREAK');
+ lineMarker.setAttribute('data-line-number', lineNumbers[i].getAttribute('data-line-number'));
+ lineMarker.setAttribute('class', lineNumbers[i].getAttribute('class'));
+ insertBefore.parentNode.insertBefore(lineMarker, insertBefore);
+ }
+ };
+
+ /*
+ * Returns an array with the following values:
+ * 0: the most specific DOM-node that contains both line numbers
+ * 1: the context of node1 (an array of dom-elements; 0 is the document fragment)
+ * 2: the context of node2 (an array of dom-elements; 0 is the document fragment)
+ * 3: the index of [0] in the two arrays
+ */
+ this._getCommonAncestor = function(node1, node2) {
+ var trace1 = this._getNodeContextTrace(node1),
+ trace2 = this._getNodeContextTrace(node2),
+ commonAncestor = null,
+ commonIndex = null,
+ childTrace1 = [],
+ childTrace2 = [];
+
+ for (var i = 0; i < trace1.length && i < trace2.length; i++) {
+ if (trace1[i] == trace2[i]) {
+ commonAncestor = trace1[i];
+ commonIndex = i;
+ }
+ }
+ for (i = commonIndex + 1; i < trace1.length; i++) {
+ childTrace1.push(trace1[i]);
+ }
+ for (i = commonIndex + 1; i < trace2.length; i++) {
+ childTrace2.push(trace2[i]);
+ }
+ return {
+ 'commonAncestor': commonAncestor,
+ 'trace1' : childTrace1,
+ 'trace2' : childTrace2,
+ 'index': commonIndex
+ };
+ };
+
+ this._serializeTag = function(node) {
+ if (node.nodeType == DOCUMENT_FRAGMENT_NODE) {
+ // Fragments are only placeholders and do not have an HTML representation
+ return '';
+ }
+ var html = '<' + node.nodeName;
+ for (var i = 0; i < node.attributes.length; i++) {
+ var attr = node.attributes[i];
+ html += " " + attr.name + "=\"" + attr.value + "\"";
+ }
+ html += '>';
+ return html;
+ };
+
+ this._serializeDom = function(node, stripLineNumbers) {
+ if (node.nodeType == TEXT_NODE) {
+ return node.nodeValue;
+ }
+ if (stripLineNumbers && (
+ lineNumberingService._isOsLineNumberNode(node) || lineNumberingService._isOsLineBreakNode(node))) {
+ return '';
+ }
+ if (node.nodeName == 'OS-LINEBREAK') {
+ return '';
+ }
+ if (node.nodeName == 'BR') {
+ var br = ' ';
+ }
+
+ var html = this._serializeTag(node);
+ for (var i = 0; i < node.childNodes.length; i++) {
+ if (node.childNodes[i].nodeType == TEXT_NODE) {
+ html += node.childNodes[i].nodeValue;
+ } else if (!stripLineNumbers || (!lineNumberingService._isOsLineNumberNode(node.childNodes[i]) && !lineNumberingService._isOsLineBreakNode(node.childNodes[i]))) {
+ html += this._serializeDom(node.childNodes[i], stripLineNumbers);
+ }
+ }
+ if (node.nodeType != DOCUMENT_FRAGMENT_NODE) {
+ html += '' + node.nodeName + '>';
+ }
+
+ return html;
+ };
+
+ /**
+ * Implementation hint: the first element of "toChildTrace" array needs to be a child element of "node"
+ */
+ this._serializePartialDomToChild = function(node, toChildTrace, stripLineNumbers) {
+ if (lineNumberingService._isOsLineNumberNode(node) || lineNumberingService._isOsLineBreakNode(node)) {
+ return '';
+ }
+ if (node.nodeName == 'OS-LINEBREAK') {
+ return '';
+ }
+
+ var html = this._serializeTag(node);
+
+ for (var i = 0, found = false; i < node.childNodes.length && !found; i++) {
+ if (node.childNodes[i] == toChildTrace[0]) {
+ found = true;
+ var remainingTrace = toChildTrace;
+ remainingTrace.shift();
+ if (!lineNumberingService._isOsLineNumberNode(node.childNodes[i])) {
+ html += this._serializePartialDomToChild(node.childNodes[i], remainingTrace, stripLineNumbers);
+ }
+ } else if (node.childNodes[i].nodeType == TEXT_NODE) {
+ html += node.childNodes[i].nodeValue;
+ } else {
+ if (!stripLineNumbers || (!lineNumberingService._isOsLineNumberNode(node.childNodes[i]) &&
+ !lineNumberingService._isOsLineBreakNode(node.childNodes[i]))) {
+ html += this._serializeDom(node.childNodes[i], stripLineNumbers);
+ }
+ }
+ }
+ if (!found) {
+ console.trace();
+ throw "Inconsistency or invalid call of this function detected";
+ }
+ return html;
+ };
+
+ /**
+ * Implementation hint: the first element of "toChildTrace" array needs to be a child element of "node"
+ */
+ this._serializePartialDomFromChild = function(node, fromChildTrace, stripLineNumbers) {
+ if (lineNumberingService._isOsLineNumberNode(node) || lineNumberingService._isOsLineBreakNode(node)) {
+ return '';
+ }
+ if (node.nodeName == 'OS-LINEBREAK') {
+ return '';
+ }
+
+ var html = '';
+ for (var i = 0, found = false; i < node.childNodes.length; i++) {
+ if (node.childNodes[i] == fromChildTrace[0]) {
+ found = true;
+ var remainingTrace = fromChildTrace;
+ remainingTrace.shift();
+ if (!lineNumberingService._isOsLineNumberNode(node.childNodes[i])) {
+ html += this._serializePartialDomFromChild(node.childNodes[i], remainingTrace, stripLineNumbers);
+ }
+ } else if (found) {
+ if (node.childNodes[i].nodeType == TEXT_NODE) {
+ html += node.childNodes[i].nodeValue;
+ } else {
+ if (!stripLineNumbers || (!lineNumberingService._isOsLineNumberNode(node.childNodes[i]) &&
+ !lineNumberingService._isOsLineBreakNode(node.childNodes[i]))) {
+ html += this._serializeDom(node.childNodes[i], stripLineNumbers);
+ }
+ }
+ }
+ }
+ if (!found) {
+ console.trace();
+ throw "Inconsistency or invalid call of this function detected";
+ }
+ if (node.nodeType != DOCUMENT_FRAGMENT_NODE) {
+ html += '' + node.nodeName + '>';
+ }
+ return html;
+ };
+
+ this.htmlToFragment = function(html) {
+ var fragment = document.createDocumentFragment(),
+ div = document.createElement('DIV');
+ div.innerHTML = html;
+ while (div.childElementCount) {
+ var child = div.childNodes[0];
+ div.removeChild(child);
+ fragment.appendChild(child);
+ }
+ return fragment;
+ };
+
+ /**
+ * Returns the HTML snippet between two given line numbers.
+ *
+ * Hint:
+ * - The last line (toLine) is not included anymore, as the number refers to the line breaking element
+ *
+ * In addition to the HTML snippet, additional information is provided regarding the most specific DOM element
+ * that contains the whole section specified by the line numbers (like a P-element if only one paragraph is selected
+ * or the most outer DIV, if multiple sections selected).
+ *
+ * This additional information is meant to render the snippet correctly without producing broken HTML
+ *
+ * The return object has the following fields:
+ * - html: The HTML between the two line numbers.
+ * Line numbers and automatically set line breaks are stripped.
+ * All HTML tags are converted to uppercase
+ * (e.g. Line 2
Line3
Line 4 )
+ * - ancestor: the most specific DOM element that contains the HTML snippet (e.g. a UL, if several LIs are selected)
+ * - outerContextStart: An HTML string that opens all necessary tags to get the browser into the rendering mode
+ * of the ancestor element (e.g.
in the case of the multiple LIs)
+ * - outerContectEnd: An HTML string that closes all necessary tags from the ancestor element (e.g.
+ * - innerContextStart: A string that opens all necessary tags between the ancestor
+ * and the beginning of the selection (e.g.
)
+ * - innerContextEnd: A string that closes all tags after the end of the selection to the ancestor (e.g.
)
+ * - previousHtml: The HTML before the selected area begins (including line numbers)
+ * - previousHtmlEndSnippet: A HTML snippet that closes all open tags from previousHtml
+ * - followingHtml: The HTML after the selected area
+ * - followingHtmlStartSnippet: A HTML snippet that opens all HTML tags necessary to render "followingHtml"
+ *
+ */
+ this.extractRangeByLineNumbers = function(fragment, fromLine, toLine) {
+ this._insertInternalLineMarkers(fragment);
+
+ var fromLineNode = this.getLineNumberNode(fragment, fromLine),
+ toLineNode = this.getLineNumberNode(fragment, toLine),
+ ancestorData = this._getCommonAncestor(fromLineNode, toLineNode);
+
+ var fromChildTraceRel = ancestorData.trace1,
+ fromChildTraceAbs = this._getNodeContextTrace(fromLineNode),
+ toChildTraceRel = ancestorData.trace2,
+ toChildTraceAbs = this._getNodeContextTrace(toLineNode),
+ ancestor = ancestorData.commonAncestor,
+ html = '',
+ outerContextStart = '',
+ outerContextEnd = '',
+ innerContextStart = '',
+ innerContextEnd = '',
+ previousHtmlEndSnippet = '',
+ followingHtmlStartSnippet = '';
+
+
+ fromChildTraceAbs.shift();
+ var previousHtml = this._serializePartialDomToChild(fragment, fromChildTraceAbs, false);
+ toChildTraceAbs.shift();
+ var followingHtml = this._serializePartialDomFromChild(fragment, toChildTraceAbs, false);
+
+ var currNode = fromLineNode.parentNode;
+ while (currNode.parentNode) {
+ previousHtmlEndSnippet += '' + currNode.nodeName + '>';
+ currNode = currNode.parentNode;
+ }
+ currNode = toLineNode.parentNode;
+ while (currNode.parentNode) {
+ followingHtmlStartSnippet = this._serializeTag(currNode) + followingHtmlStartSnippet;
+ currNode = currNode.parentNode;
+ }
+
+ var found = false;
+ for (var i = 0; i < fromChildTraceRel.length && !found; i++) {
+ if (fromChildTraceRel[i].nodeName == 'OS-LINEBREAK') {
+ found = true;
+ } else {
+ innerContextStart += this._serializeTag(fromChildTraceRel[i]);
+ }
+ }
+ found = false;
+ for (i = 0; i < toChildTraceRel.length && !found; i++) {
+ if (toChildTraceRel[i].nodeName == 'OS-LINEBREAK') {
+ found = true;
+ } else {
+ innerContextEnd = '' + toChildTraceRel[i].nodeName + '>' + innerContextEnd;
+ }
+ }
+
+ found = false;
+ for (i = 0; i < ancestor.childNodes.length; i++) {
+ if (ancestor.childNodes[i] == fromChildTraceRel[0]) {
+ found = true;
+ fromChildTraceRel.shift();
+ html += this._serializePartialDomFromChild(ancestor.childNodes[i], fromChildTraceRel, true);
+ } else if (ancestor.childNodes[i] == toChildTraceRel[0]) {
+ found = false;
+ toChildTraceRel.shift();
+ html += this._serializePartialDomToChild(ancestor.childNodes[i], toChildTraceRel, true);
+ } else if (found === true) {
+ html += this._serializeDom(ancestor.childNodes[i], true);
+ }
+ }
+
+ currNode = ancestor;
+ while (currNode.parentNode) {
+ outerContextStart = this._serializeTag(currNode) + outerContextStart;
+ outerContextEnd += '' + currNode.nodeName + '>';
+ currNode = currNode.parentNode;
+ }
+
+ return {
+ 'html': html,
+ 'ancestor': ancestor,
+ 'outerContextStart': outerContextStart,
+ 'outerContextEnd': outerContextEnd,
+ 'innerContextStart': innerContextStart,
+ 'innerContextEnd': innerContextEnd,
+ 'previousHtml': previousHtml,
+ 'previousHtmlEndSnippet': previousHtmlEndSnippet,
+ 'followingHtml': followingHtml,
+ 'followingHtmlStartSnippet': followingHtmlStartSnippet
+ };
+
+ };
+
+ this._replaceLinesMergeNodeArrays = function(nodes1, nodes2) {
+ if (nodes1.length === 0) {
+ return nodes2;
+ }
+ if (nodes2.length === 0) {
+ return nodes1;
+ }
+
+ var out = [];
+ for (var i = 0; i < nodes1.length - 1; i++) {
+ out.push(nodes1[i]);
+ }
+
+ out.push(nodes1[nodes1.length - 1]);
+ out.push(nodes2[0]);
+
+ for (i = 1; i < nodes2.length; i++) {
+ out.push(nodes2[i]);
+ }
+
+ /*
+ if (node1.nodeName != node2.nodeName) {
+ return null;
+ }
+ var newNode = node1.ownerDocument.createElement(node1.nodeName);
+ for (var i = 0; i < node1.attributes.length; i++) {
+ var attr = node1.attributes[i];
+ newNode.setAttribute(attr.name, attr.value);
+ }
+ return newNode;
+ */
+ return out;
+ };
+
+ this.replaceLines = function (fragment, newHTML, fromLine, toLine) {
+ var data = this.extractRangeByLineNumbers(fragment, fromLine, toLine),
+ previousHtml = data.previousHtml + data.previousHtmlEndSnippet,
+ previousFragment = this.htmlToFragment(previousHtml),
+ followingHtml = data.followingHtmlStartSnippet + data.followingHtml,
+ followingFragment = this.htmlToFragment(followingHtml),
+ newFragment = this.htmlToFragment(newHTML),
+ child;
+
+ var merged = document.createDocumentFragment();
+
+ while (previousFragment.children.length > 0) {
+ child = previousFragment.children[0];
+ previousFragment.removeChild(child);
+ merged.appendChild(child);
+ }
+ while (newFragment.children.length > 0) {
+ child = newFragment.children[0];
+ newFragment.removeChild(child);
+ merged.appendChild(child);
+ }
+ while (followingFragment.children.length > 0) {
+ child = followingFragment.children[0];
+ followingFragment.removeChild(child);
+ merged.appendChild(child);
+ }
+ //var merged = this._replaceLinesAttemptMerge(lastOfPrevious, firstOfReplaced);
+
+ return this._serializeDom(merged, true);
+ };
+});
+
+
+}());
diff --git a/openslides/motions/static/js/motions/linenumbering.js b/openslides/motions/static/js/motions/linenumbering.js
new file mode 100644
index 000000000..f4038ed1d
--- /dev/null
+++ b/openslides/motions/static/js/motions/linenumbering.js
@@ -0,0 +1,329 @@
+(function () {
+
+"use strict";
+
+angular.module('OpenSlidesApp.motions.lineNumbering', [])
+
+/**
+ * Current limitations of this implementation:
+ *
+ * Only the following inline elements are supported:
+ * - 'SPAN', 'A', 'EM', 'S', 'B', 'I', 'STRONG', 'U', 'BIG', 'SMALL', 'SUB', 'SUP', 'TT'
+ *
+ * Only other inline elements are allowed within inline elements.
+ * No constructs like are allowed. CSS-attributes like 'display: block' are ignored.
+ */
+
+.service('lineNumberingService', function () {
+ var ELEMENT_NODE = 1,
+ TEXT_NODE = 3;
+
+ this._currentInlineOffset = null;
+ this._currentLineNumber = null;
+ this._prependLineNumberToFirstText = false;
+
+ this._isInlineElement = function (node) {
+ var inlineElements = [
+ 'SPAN', 'A', 'EM', 'S', 'B', 'I', 'STRONG', 'U', 'BIG', 'SMALL', 'SUB', 'SUP', 'TT'
+ ];
+ return (inlineElements.indexOf(node.nodeName) > -1);
+ };
+
+ this._isOsLineBreakNode = function (node) {
+ var isLineBreak = false;
+ if (node && node.nodeType === ELEMENT_NODE && node.nodeName == 'BR' && node.hasAttribute('class')) {
+ var classes = node.getAttribute('class').split(' ');
+ if (classes.indexOf('os-line-break') > -1) {
+ isLineBreak = true;
+ }
+ }
+ return isLineBreak;
+ };
+
+ this._isOsLineNumberNode = function (node) {
+ var isLineNumber = false;
+ if (node && node.nodeType === ELEMENT_NODE && node.nodeName == 'SPAN' && node.hasAttribute('class')) {
+ var classes = node.getAttribute('class').split(' ');
+ if (classes.indexOf('os-line-number') > -1) {
+ isLineNumber = true;
+ }
+ }
+ return isLineNumber;
+ };
+
+ /**
+ * Splits a TEXT_NODE into an array of TEXT_NODEs and BR-Elements separating them into lines.
+ * Each line has a maximum length of 'length', with one exception: spaces are accepted to exceed the length.
+ * Otherwise the string is split by the last space or dash in the line.
+ *
+ * @param node
+ * @param length
+ * @returns Array
+ * @private
+ */
+ this._textNodeToLines = function (node, length) {
+ var out = [],
+ currLineStart = 0,
+ i = 0,
+ firstTextNode = true,
+ lastBreakableIndex = null,
+ service = this;
+
+ var createLineBreak = function() {
+ var br = document.createElement('br');
+ br.setAttribute('class', 'os-line-break');
+ return br;
+ };
+ var createLineNumber = function() {
+ var node = document.createElement('span');
+ var lineNumber = service._currentLineNumber;
+ service._currentLineNumber++;
+ node.setAttribute('class', 'os-line-number line-number-' + lineNumber);
+ node.setAttribute('data-line-number', lineNumber + '');
+ node.setAttribute('contenteditable', 'false');
+ node.innerHTML = ' '; // Prevent tinymce from stripping out empty span's
+ return node;
+ };
+ var addLine = function (text) {
+ var newNode = document.createTextNode(text);
+ if (firstTextNode) {
+ firstTextNode = false;
+ } else {
+ out.push(createLineBreak());
+ out.push(createLineNumber());
+ }
+ out.push(newNode);
+ };
+
+ if (node.nodeValue == "\n") {
+ out.push(node);
+ } else {
+
+ // This happens if a previous inline element exactly stretches to the end of the line
+ if (this._currentInlineOffset >= length) {
+ out.push(createLineBreak());
+ out.push(createLineNumber());
+ this._currentInlineOffset = 0;
+ } else if (this._prependLineNumberToFirstText) {
+ out.push(createLineNumber());
+ }
+ this._prependLineNumberToFirstText = false;
+
+ while (i < node.nodeValue.length) {
+ var lineBreakAt = null;
+ if (this._currentInlineOffset >= length) {
+ if (lastBreakableIndex !== null) {
+ lineBreakAt = lastBreakableIndex;
+ } else {
+ lineBreakAt = i - 1;
+ }
+ }
+ if (lineBreakAt !== null && node.nodeValue[i] != ' ') {
+ var currLine = node.nodeValue.substring(currLineStart, lineBreakAt + 1);
+ addLine(currLine);
+
+ currLineStart = lineBreakAt + 1;
+ this._currentInlineOffset = i - lineBreakAt - 1;
+ lastBreakableIndex = null;
+ }
+
+ if (node.nodeValue[i] == ' ' || node.nodeValue[i] == '-') {
+ lastBreakableIndex = i;
+ }
+
+ this._currentInlineOffset++;
+ i++;
+
+ }
+ addLine(node.nodeValue.substring(currLineStart));
+ }
+ return out;
+ };
+
+
+ /**
+ * Moves line breaking and line numbering markup before inline elements
+ *
+ * @param innerNode
+ * @param outerNode
+ * @private
+ */
+ this._moveLeadingLineBreaksToOuterNode = function (innerNode, outerNode) {
+ if (this._isInlineElement(innerNode)) {
+ if (this._isOsLineBreakNode(innerNode.firstChild)) {
+ var br = innerNode.firstChild;
+ innerNode.removeChild(br);
+ outerNode.appendChild(br);
+ }
+ if (this._isOsLineNumberNode(innerNode.firstChild)) {
+ var span = innerNode.firstChild;
+ innerNode.removeChild(span);
+ outerNode.appendChild(span);
+ }
+ }
+ };
+
+
+ this._insertLineNumbersToInlineNode = function (node, length) {
+ var oldChildren = [], i;
+ for (i = 0; i < node.childNodes.length; i++) {
+ oldChildren.push(node.childNodes[i]);
+ }
+
+ while (node.firstChild) {
+ node.removeChild(node.firstChild);
+ }
+
+ for (i = 0; i < oldChildren.length; i++) {
+ if (oldChildren[i].nodeType == TEXT_NODE) {
+ var ret = this._textNodeToLines(oldChildren[i], length);
+ for (var j = 0; j < ret.length; j++) {
+ node.appendChild(ret[j]);
+ }
+ } else if (oldChildren[i].nodeType == ELEMENT_NODE) {
+ var changedNode = this._insertLineNumbersToNode(oldChildren[i], length);
+ this._moveLeadingLineBreaksToOuterNode(changedNode, node);
+ node.appendChild(changedNode);
+ } else {
+ throw 'Unknown nodeType: ' + i + ': ' + oldChildren[i];
+ }
+ }
+
+ return node;
+ };
+
+ this._calcBlockNodeLength = function (node, oldLength) {
+ var newLength = oldLength;
+ switch (node.nodeName) {
+ case 'LI':
+ newLength -= 5;
+ break;
+ case 'BLOCKQUOTE':
+ newLength -= 20;
+ break;
+ case 'DIV':
+ case 'P':
+ var styles = node.getAttribute("style"),
+ padding = 0;
+ if (styles) {
+ var leftpad = styles.split("padding-left:");
+ if (leftpad.length > 1) {
+ leftpad = parseInt(leftpad[1]);
+ padding += leftpad;
+ }
+ var rightpad = styles.split("padding-right:");
+ if (rightpad.length > 1) {
+ rightpad = parseInt(rightpad[1]);
+ padding += rightpad;
+ }
+ newLength -= (padding / 5);
+ }
+ break;
+ case 'H1':
+ newLength *= 0.5;
+ break;
+ case 'H2':
+ newLength *= 0.66;
+ break;
+ case 'H3':
+ newLength *= 0.66;
+ break;
+ }
+ return Math.ceil(newLength);
+ };
+
+ this._insertLineNumbersToBlockNode = function (node, length) {
+ this._currentInlineOffset = 0;
+ this._prependLineNumberToFirstText = true;
+
+ var oldChildren = [], i;
+ for (i = 0; i < node.childNodes.length; i++) {
+ oldChildren.push(node.childNodes[i]);
+ }
+
+ while (node.firstChild) {
+ node.removeChild(node.firstChild);
+ }
+
+ for (i = 0; i < oldChildren.length; i++) {
+ if (oldChildren[i].nodeType == TEXT_NODE) {
+ var ret = this._textNodeToLines(oldChildren[i], length);
+ for (var j = 0; j < ret.length; j++) {
+ node.appendChild(ret[j]);
+ }
+ } else if (oldChildren[i].nodeType == ELEMENT_NODE) {
+ var changedNode = this._insertLineNumbersToNode(oldChildren[i], length);
+ this._moveLeadingLineBreaksToOuterNode(changedNode, node);
+ node.appendChild(changedNode);
+ } else {
+ throw 'Unknown nodeType: ' + i + ': ' + oldChildren[i];
+ }
+ }
+
+ this._currentInlineOffset = 0;
+ this._prependLineNumberToFirstText = true;
+
+ return node;
+ };
+
+ this._insertLineNumbersToNode = function (node, length) {
+ if (node.nodeType !== ELEMENT_NODE) {
+ throw 'This method may only be called for ELEMENT-nodes: ' + node.nodeValue;
+ }
+ if (this._isInlineElement(node)) {
+ return this._insertLineNumbersToInlineNode(node, length);
+ } else {
+ var newLength = this._calcBlockNodeLength(node, length);
+ return this._insertLineNumbersToBlockNode(node, newLength);
+ }
+ };
+
+ this._stripLineNumbers = function (node) {
+
+ for (var i = 0; i < node.childNodes.length; i++) {
+ if (this._isOsLineBreakNode(node.childNodes[i]) || this._isOsLineNumberNode(node.childNodes[i])) {
+ node.removeChild(node.childNodes[i]);
+ i--;
+ } else {
+ this._stripLineNumbers(node.childNodes[i]);
+ }
+ }
+ };
+
+ this._nodesToHtml = function (nodes) {
+ var root = document.createElement('div');
+ for (var i in nodes) {
+ if (nodes.hasOwnProperty(i)) {
+ root.appendChild(nodes[i]);
+ }
+ }
+ return root.innerHTML;
+ };
+
+ this.insertLineNumbersNode = function (html, lineLength) {
+ var root = document.createElement('div');
+ root.innerHTML = html;
+
+ this._currentInlineOffset = 0;
+ this._currentLineNumber = 1;
+ this._prependLineNumberToFirstText = true;
+
+ return this._insertLineNumbersToNode(root, lineLength);
+ };
+
+ this.insertLineNumbers = function (html, lineLength) {
+ var newRoot = this.insertLineNumbersNode(html, lineLength);
+
+ return newRoot.innerHTML;
+ };
+
+ this.stripLineNumbers = function (html) {
+ var root = document.createElement('div');
+ root.innerHTML = html;
+ this._stripLineNumbers(root);
+ return root.innerHTML;
+ };
+});
+
+
+}());
diff --git a/openslides/motions/static/js/motions/projector.js b/openslides/motions/static/js/motions/projector.js
index 23efa505b..bb4c91c87 100644
--- a/openslides/motions/static/js/motions/projector.js
+++ b/openslides/motions/static/js/motions/projector.js
@@ -17,7 +17,8 @@ angular.module('OpenSlidesApp.motions.projector', ['OpenSlidesApp.motions'])
'$scope',
'Motion',
'User',
- function($scope, Motion, User) {
+ 'Config',
+ function($scope, Motion, User, Config) {
// Attention! Each object that is used here has to be dealt on server side.
// Add it to the coresponding get_requirements method of the ProjectorElement
// class.
@@ -32,6 +33,8 @@ angular.module('OpenSlidesApp.motions.projector', ['OpenSlidesApp.motions'])
// load all users
User.findAll();
User.bindAll({}, $scope, 'users');
+
+ Config.bindOne('motions_default_line_numbering', $scope, 'line_numbering');
}
]);
diff --git a/openslides/motions/static/js/motions/site.js b/openslides/motions/static/js/motions/site.js
index b5d3cf109..4ed2ecc4f 100644
--- a/openslides/motions/static/js/motions/site.js
+++ b/openslides/motions/static/js/motions/site.js
@@ -2,7 +2,7 @@
'use strict';
-angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
+angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlidesApp.motions.diff'])
.factory('MotionContentProvider', ['gettextCatalog', function(gettextCatalog) {
/**
@@ -767,6 +767,7 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
.controller('MotionDetailCtrl', [
'$scope',
'$http',
+ '$timeout',
'ngDialog',
'MotionForm',
'Motion',
@@ -775,16 +776,18 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
'Tag',
'User',
'Workflow',
+ 'Editor',
+ 'Config',
'motion',
'SingleMotionContentProvider',
'MotionContentProvider',
'PdfMakeConverter',
'PdfMakeDocumentProvider',
'gettextCatalog',
- function($scope, $http, ngDialog, MotionForm,
- Motion, Category, Mediafile, Tag,
- User, Workflow, motion,
- SingleMotionContentProvider, MotionContentProvider, PdfMakeConverter, PdfMakeDocumentProvider, gettextCatalog) {
+ 'diffService',
+ function($scope, $http, $timeout, ngDialog, MotionForm, Motion, Category, Mediafile, Tag, User, Workflow, Editor,
+ Config,motion, SingleMotionContentProvider, MotionContentProvider, PdfMakeConverter,
+ PdfMakeDocumentProvider, gettextCatalog, diffService) {
Motion.bindOne(motion.id, $scope, 'motion');
Category.bindAll({}, $scope, 'categories');
Mediafile.bindAll({}, $scope, 'mediafiles');
@@ -794,6 +797,8 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
Motion.loadRelations(motion, 'agenda_item');
$scope.version = motion.active_version;
$scope.isCollapsed = true;
+ $scope.lineNumberMode = Config.get('motions_default_line_numbering').value;
+ $scope.lineBrokenText = motion.getTextWithLineBreaks($scope.version);
$scope.makePDF = function(){
var content = motion.getText($scope.version) + motion.getReason($scope.version),
@@ -824,6 +829,9 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
// open edit dialog
$scope.openDialog = function (motion) {
+ if ($scope.inlineEditing.active) {
+ $scope.disableInlineEditing();
+ }
ngDialog.open(MotionForm.getDialog(motion));
};
// support
@@ -871,13 +879,24 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
// show specific version
$scope.showVersion = function (version) {
$scope.version = version.id;
+ $scope.lineBrokenText = motion.getTextWithLineBreaks($scope.version);
+ $scope.inlineEditing.allowed = (motion.isAllowed('update') && $scope.version == motion.getVersion(-1).id);
+ $scope.inlineEditing.changed = false;
+ $scope.inlineEditing.active = false;
+ if ($scope.inlineEditing.editor) {
+ $scope.inlineEditing.editor.setContent($scope.lineBrokenText);
+ $scope.inlineEditing.editor.setMode("readonly");
+ $scope.inlineEditing.originalHtml = $scope.inlineEditing.editor.getContent();
+ } else {
+ $scope.inlineEditing.originalHtml = $scope.lineBrokenText;
+ }
};
// permit specific version
$scope.permitVersion = function (version) {
$http.put('/rest/motions/motion/' + motion.id + '/manage_version/',
{'version_number': version.version_number})
.then(function(success) {
- $scope.version = version.id;
+ $scope.showVersion(version);
});
};
// delete specific version
@@ -886,9 +905,101 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
{headers: {'Content-Type': 'application/json'},
data: JSON.stringify({version_number: version.version_number})})
.then(function(success) {
- $scope.version = motion.active_version;
+ $scope.showVersion(motion.active_version);
});
};
+
+ // Inline editing functions
+ $scope.inlineEditing = {
+ allowed: (motion.isAllowed('update') && $scope.version == motion.getVersion(-1).id),
+ active: false,
+ changed: false,
+ trivialChange: false,
+ trivialChangeAllowed: false,
+ editor: null,
+ originalHtml: $scope.lineBrokenText,
+ };
+
+ if (motion.state.versioning && Config.get('motions_allow_disable_versioning').value) {
+ $scope.inlineEditing.trivialChange = true;
+ $scope.inlineEditing.trivialChangeAllowed = true;
+ }
+
+ $scope.$watch(
+ function () {
+ return Motion.lastModified();
+ },
+ function () {
+ $scope.inlineEditing.trivialChangeAllowed =
+ (motion.state.versioning && Config.get('motions_allow_disable_versioning').value);
+ }
+ );
+
+ $scope.tinymceOptions = Editor.getOptions(null, true);
+ $scope.tinymceOptions.readonly = 1;
+ $scope.tinymceOptions.setup = function (editor) {
+ $scope.inlineEditing.editor = editor;
+ editor.on("init", function () {
+ $scope.lineBrokenText = motion.getTextWithLineBreaks($scope.version);
+ $scope.inlineEditing.editor.setContent($scope.lineBrokenText);
+ $scope.inlineEditing.originalHtml = $scope.inlineEditing.editor.getContent();
+ $scope.inlineEditing.changed = false;
+ });
+ editor.on("change", function () {
+ $scope.inlineEditing.changed = (editor.getContent() != $scope.inlineEditing.originalHtml);
+ });
+ editor.on("undo", function() {
+ $scope.inlineEditing.changed = (editor.getContent() != $scope.inlineEditing.originalHtml);
+ });
+ };
+
+ $scope.enableInlineEditing = function() {
+ $scope.inlineEditing.editor.setMode("design");
+ $scope.inlineEditing.active = true;
+ $scope.inlineEditing.changed = false;
+
+ $scope.lineBrokenText = motion.getTextWithLineBreaks($scope.version);
+ $scope.inlineEditing.editor.setContent($scope.lineBrokenText);
+ $scope.inlineEditing.originalHtml = $scope.inlineEditing.editor.getContent();
+ $timeout(function() {
+ $scope.inlineEditing.editor.focus();
+ }, 100);
+ };
+
+ $scope.disableInlineEditing = function() {
+ $scope.inlineEditing.editor.setMode("readonly");
+ $scope.inlineEditing.active = false;
+ $scope.inlineEditing.changed = false;
+ $scope.lineBrokenText = $scope.inlineEditing.originalHtml;
+ $scope.inlineEditing.editor.setContent($scope.inlineEditing.originalHtml);
+ };
+
+ $scope.motionInlineSave = function () {
+ if (!$scope.inlineEditing.allowed) {
+ throw "No permission to update motion";
+ }
+
+ motion.setTextStrippingLineBreaks(motion.active_version, $scope.inlineEditing.editor.getContent());
+ motion.disable_versioning = $scope.inlineEditing.trivialChange;
+
+ Motion.inject(motion);
+ // save change motion object on server
+ Motion.save(motion, { method: 'PATCH' }).then(
+ function(success) {
+ $scope.showVersion(motion.getVersion(-1));
+ },
+ function (error) {
+ // save error: revert all changes by restore
+ // (refresh) original motion object from server
+ Motion.refresh(motion);
+ var message = '';
+ for (var e in error.data) {
+ message += e + ': ' + error.data[e] + ' ';
+ }
+ $scope.alert = {type: 'danger', msg: message, show: true};
+ }
+ );
+ };
}
])
@@ -1389,6 +1500,15 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions'])
gettext('Show paragraph numbering (only in PDF)');
/// Prefix for the identifier for amendments
gettext('A');
+ gettext('Default line numbering');
+ /// Line numbering: Outside
+ gettext('Outside');
+ /// Line numbering: Inline
+ gettext('Inline');
+ /// Line numbering: None
+ gettext('None');
+ gettext('Line length');
+ gettext('The maximum number of characters per line. Relevant when line numbering is enabled. Min: 40');
}
]);
diff --git a/openslides/motions/static/templates/motions/motion-detail.html b/openslides/motions/static/templates/motions/motion-detail.html
index cbf62a254..8f4fdd080 100644
--- a/openslides/motions/static/templates/motions/motion-detail.html
+++ b/openslides/motions/static/templates/motions/motion-detail.html
@@ -227,9 +227,66 @@
sdfsdfsdfsdf dsfsdfsdfdsflkewjrl ksjfl ksdjf klnlkjBavaria ipsum dolor sit amet Biazelt Auffisteign Schorsch mim Radl foahn Ohrwaschl Steckerleis wann griagd ma nacha wos z’dringa glacht Mamalad, ' +
+ 'muass? I bin a woschechta Bayer sowos oamoi und sei und glei wirds no fui lustiga: Jo mei khkhis des schee middn ognudelt, Trachtnhuat Biawambn gscheid: Griasd eich midnand etza nix Gwiass woass ma ned ' +
+ 'owe. Dahoam gscheckate middn Spuiratz des is a gmahde Wiesn. Des is schee so Obazda san da, Haferl pfenningguat schoo griasd eich midnand.
\
+
\
+
Auffi Gamsbart nimma de Sepp Ledahosn Ohrwaschl um Godds wujn Wiesn Deandlgwand Mongdratzal! Jo leck mi Mamalad i daad mechad?
\
+
Do nackata Wurscht i hob di narrisch gean, Diandldrahn Deandlgwand vui huift vui woaß?
\
+
Ned Mamalad auffi i bin a woschechta Bayer greaßt eich nachad, umananda gwiss nia need Weiznglasl.
\
+
Woibbadinga noch da Giasinga Heiwog Biazelt mechad mim Spuiratz, soi zwoa.
\
+
\
+
I waar soweid Blosmusi es nomoi. Broadwurschtbudn des is a gmahde Wiesn Kirwa mogsd a Bussal Guglhupf schüds nei. Luja i moan oiwei Baamwach Watschnbaam, wiavui baddscher! Biakriagal a fescha ' +
+ '1Bua Semmlkneedl iabaroi oba um Godds wujn Ledahosn wui Greichats. Geh um Godds wujn luja heid greaßt eich nachad woaß Breihaus eam! De om auf’n Gipfe auf gehds beim Schichtl mehra Baamwach a bissal wos gehd ollaweil gscheid:
\
+
\
+
Scheans Schdarmbeaga See i hob di narrisch gean i jo mei is des schee! Nia eam hod vasteh i sog ja nix, i red ja bloß sammawiedaguad, umma eana obandeln! Zwoa jo mei scheans amoi, san und hoggd Milli barfuaßat gscheit. Foidweg vui huift ' +
+ 'vui singan, mehra Biakriagal om auf’n Gipfe! Ozapfa sodala Charivari greaßt eich nachad Broadwurschtbudn do middn liberalitas Bavariae sowos Leonhardifahrt:
\
+
\
+
Wui helfgod Wiesn, ognudelt schaugn: Dahoam gelbe Rüam Schneid singan wo hi sauba i moan scho aa no a Maß a Maß und no a Maß nimma. Is umananda a ganze Hoiwe zwoa, Schneid. Vui huift vui Brodzeid kumm ' +
+ 'geh naa i daad vo de allerweil, gor. Woaß wia Gams, damischa. A ganze Hoiwe Ohrwaschl Greichats iabaroi Prosd Engelgwand nix Reiwadatschi.Weibaleid ognudelt Ledahosn noch da Giasinga Heiwog i daad ' +
+ 'Almrausch, Ewig und drei Dog nackata wea ko, dea ko. Meidromml Graudwiggal nois dei, nackata. No Diandldrahn nix Gwiass woass ma ned hod boarischer: Samma sammawiedaguad wos, i hoam Brodzeid. Jo ' +
+ 'mei Sepp Gaudi, is ma Wuascht do Hendl Xaver Prosd eana an a bravs. Sauwedda an Brezn, abfieseln.
');
+ });
+
+ it('extracts lines from a more complex example', function () {
+ var diff = diffService.extractRangeByLineNumbers(baseHtmlDom2, 6, 11, true);
+
+ 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.
Auffi Gamsbart nimma de Sepp Ledahosn Ohrwaschl um Godds wujn Wiesn Deandlgwand Mongdratzal! Jo leck mi Mamalad i daad mechad?
Do nackata Wurscht i hob di narrisch gean, Diandldrahn Deandlgwand vui huift vui woaß?
');
+ });
+
+ });
+
+ describe('merging lines into the original motion', function () {
+
+ it('replaces LIs by a P', function () {
+ var merged = diffService.replaceLines(baseHtmlDom1, '
Replaced a UL by a P
', 6, 9);
+ expect(merged).toBe('
Line 1 Line 2Line 3 Line 4 Line 5
Replaced a UL by a P
Level 2 LI 9
Line 10 Line 11
');
+ });
+ /*
+ it('replaces LIs by another LI', function () {
+ var merged = diffService.replaceLines(baseHtmlDom1, '
A new LI
', 6, 9);
+ expect(merged).toBe('');
+ });
+ */
+
+ });
+});
diff --git a/tests/karma/motions/linenumbering.service.test.js b/tests/karma/motions/linenumbering.service.test.js
new file mode 100644
index 000000000..812359269
--- /dev/null
+++ b/tests/karma/motions/linenumbering.service.test.js
@@ -0,0 +1,216 @@
+describe('linenumbering', function () {
+
+ beforeEach(module('OpenSlidesApp.motions.lineNumbering'));
+
+ var lineNumberingService,
+ brMarkup = function (no) {
+ return ' ' +
+ '';
+ },
+ noMarkup = function (no) {
+ return '';
+ },
+ longstr = function (length) {
+ var outstr = '';
+ for (var i = 0; i < length; i++) {
+ outstr += String.fromCharCode(65 + (i % 26));
+ }
+ return outstr;
+ };
+
+ beforeEach(inject(function (_lineNumberingService_) {
+ lineNumberingService = _lineNumberingService_;
+ }));
+
+ describe('line numbering: test nodes', function () {
+ it('breaks very short lines', function () {
+ var textNode = document.createTextNode("0123");
+ lineNumberingService._currentInlineOffset = 0;
+ var out = lineNumberingService._textNodeToLines(textNode, 5);
+ var outHtml = lineNumberingService._nodesToHtml(out);
+ expect(outHtml).toBe('0123');
+ expect(lineNumberingService._currentInlineOffset).toBe(4);
+ });
+
+ it('breaks simple lines', function () {
+ var textNode = document.createTextNode("012345678901234567");
+ lineNumberingService._currentInlineOffset = 0;
+ lineNumberingService._currentLineNumber = 1;
+ var out = lineNumberingService._textNodeToLines(textNode, 5);
+ var outHtml = lineNumberingService._nodesToHtml(out);
+ expect(outHtml).toBe('01234' + brMarkup(1) + '56789' + brMarkup(2) + '01234' + brMarkup(3) + '567');
+ expect(lineNumberingService._currentInlineOffset).toBe(3);
+ });
+
+ it('breaks simple lines with offset', function () {
+ var textNode = document.createTextNode("012345678901234567");
+ lineNumberingService._currentInlineOffset = 2;
+ lineNumberingService._currentLineNumber = 1;
+ var out = lineNumberingService._textNodeToLines(textNode, 5);
+ var outHtml = lineNumberingService._nodesToHtml(out);
+ expect(outHtml).toBe('012' + brMarkup(1) + '34567' + brMarkup(2) + '89012' + brMarkup(3) + '34567');
+ expect(lineNumberingService._currentInlineOffset).toBe(5);
+ });
+
+ it('breaks simple lines with offset equaling to length', function () {
+ var textNode = document.createTextNode("012345678901234567");
+ lineNumberingService._currentInlineOffset = 5;
+ lineNumberingService._currentLineNumber = 1;
+ var out = lineNumberingService._textNodeToLines(textNode, 5);
+ var outHtml = lineNumberingService._nodesToHtml(out);
+ expect(outHtml).toBe(brMarkup(1) + '01234' + brMarkup(2) + '56789' + brMarkup(3) + '01234' + brMarkup(4) + '567');
+ expect(lineNumberingService._currentInlineOffset).toBe(3);
+ });
+
+ it('breaks simple lines with spaces (1)', function () {
+ var textNode = document.createTextNode("0123 45 67 89012 34 567");
+ lineNumberingService._currentInlineOffset = 0;
+ lineNumberingService._currentLineNumber = 1;
+ var out = lineNumberingService._textNodeToLines(textNode, 5);
+ var outHtml = lineNumberingService._nodesToHtml(out);
+ expect(outHtml).toBe('0123 ' + brMarkup(1) + '45 67 ' + brMarkup(2) + '89012 ' + brMarkup(3) + '34 ' + brMarkup(4) + '567');
+ expect(lineNumberingService._currentInlineOffset).toBe(3);
+ });
+
+ it('breaks simple lines with spaces (2)', function () {
+ var textNode = document.createTextNode("0123 45 67 89012tes 344 ");
+ lineNumberingService._currentInlineOffset = 0;
+ lineNumberingService._currentLineNumber = 1;
+ var out = lineNumberingService._textNodeToLines(textNode, 5);
+ var outHtml = lineNumberingService._nodesToHtml(out);
+ expect(outHtml).toBe('0123 ' + brMarkup(1) + '45 67 ' + brMarkup(2) + '89012' + brMarkup(3) + 'tes ' + brMarkup(4) + '344 ');
+ expect(lineNumberingService._currentInlineOffset).toBe(4);
+ });
+
+ it('breaks simple lines with spaces (3)', function () {
+ var textNode = document.createTextNode("I'm a Demo-Text");
+ lineNumberingService._currentInlineOffset = 0;
+ lineNumberingService._currentLineNumber = 1;
+ var out = lineNumberingService._textNodeToLines(textNode, 5);
+ var outHtml = lineNumberingService._nodesToHtml(out);
+ expect(outHtml).toBe('I\'m a ' + brMarkup(1) + 'Demo-' + brMarkup(2) + 'Text');
+ expect(lineNumberingService._currentInlineOffset).toBe(4);
+ });
+
+ it('breaks simple lines with spaces (4)', function () {
+ var textNode = document.createTextNode("I'm a LongDemo-Text");
+ lineNumberingService._currentInlineOffset = 0;
+ lineNumberingService._currentLineNumber = 1;
+ var out = lineNumberingService._textNodeToLines(textNode, 5);
+ var outHtml = lineNumberingService._nodesToHtml(out);
+ expect(outHtml).toBe('I\'m a ' + brMarkup(1) + 'LongD' + brMarkup(2) + 'emo-' + brMarkup(3) + 'Text');
+ expect(lineNumberingService._currentInlineOffset).toBe(4);
+ });
+ });
+
+
+ describe('line numbering: inline nodes', function () {
+ it('leaves a simple SPAN untouched', function () {
+ var inHtml = "Test";
+ var outHtml = lineNumberingService.insertLineNumbers(inHtml, 5);
+ expect(outHtml).toBe(noMarkup(1) + 'Test');
+ expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml);
+ });
+
+ it('breaks lines in a simple SPAN', function () {
+ var inHtml = "Lorem ipsum dolorsit amet";
+ var outHtml = lineNumberingService.insertLineNumbers(inHtml, 5);
+ expect(outHtml).toBe(noMarkup(1) + 'Lorem ' + brMarkup(2) + 'ipsum ' + brMarkup(3) + 'dolor' + brMarkup(4) + 'sit ' + brMarkup(5) + 'amet');
+ expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml);
+ });
+
+ it('breaks lines in nested inline elements', function () {
+ var inHtml = "Lorem ipsum dolorsit amet";
+ var outHtml = lineNumberingService.insertLineNumbers(inHtml, 5);
+ expect(outHtml).toBe(noMarkup(1) + 'Lorem ' + brMarkup(2) + 'ipsum ' + brMarkup(3) + 'dolor' + brMarkup(4) + 'sit ' + brMarkup(5) + 'amet');
+ expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml);
+ });
+ });
+
+
+ describe('line numbering: block nodes', function () {
+ it('leaves a simple DIV untouched', function () {
+ var inHtml = "
Test
";
+ var outHtml = lineNumberingService.insertLineNumbers(inHtml, 5);
+ expect(outHtml).toBe('
' + noMarkup(1) + 'Test
');
+ expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml);
+ });
+
+ it('breaks a DIV containing only inline elements', function () {
+ var inHtml = "
Test Test12345678 Test
";
+ var outHtml = lineNumberingService.insertLineNumbers(inHtml, 5);
+ expect(outHtml).toBe('
');
+ expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml);
+ });
+
+ it('handles a DIV within a DIV correctly', function () {
+ var inHtml = "
Te
Te Test
Test
";
+ var outHtml = lineNumberingService.insertLineNumbers(inHtml, 5);
+ expect(outHtml).toBe('
' + noMarkup(1) + 'Te
' + noMarkup(2) + 'Te ' + brMarkup(3) + 'Test
' + noMarkup(4) + 'Test
');
+ expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml);
+ });
+
+ it('ignores white spaces between block element tags', function () {
+ var inHtml = "
\n
Test
\n
";
+ var outHtml = lineNumberingService.insertLineNumbers(inHtml, 80);
+ expect(outHtml).toBe("
\n
" + noMarkup(1) + 'Test
\n
');
+ expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml);
+ });
+ });
+
+
+ describe('indentation for block elements', function () {
+ it('indents LI-elements', function () {
+ var inHtml = '
';
+ var outHtml = lineNumberingService.insertLineNumbers(inHtml, 80);
+ expect(outHtml).toBe(expected);
+ expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml);
+ });
+
+ it('shortens the line for H1-elements by 1/2', function () {
+ var inHtml = '
';
+ var outHtml = lineNumberingService.insertLineNumbers(inHtml, 80);
+ expect(outHtml).toBe(expected);
+ expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml);
+ });
+
+ it('shortens the line for H2-elements by 2/3', function () {
+ var inHtml = '