Merge pull request #2292 from CatoTH/line-numbers-merged

Line numbers and inline editing
This commit is contained in:
Norman Jäckel 2016-08-20 11:36:28 +02:00 committed by GitHub
commit 81570318b2
17 changed files with 1621 additions and 18 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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;
}

View File

@ -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,

View File

@ -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.

View File

@ -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,

View File

@ -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;
},

View File

@ -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 = '<BR';
for (i = 0; i < node.attributes.length; i++) {
var attr = node.attributes[i];
br += " " + attr.name + "=\"" + attr.value + "\"";
}
return 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</LI><LI>Line3</LI><LI>Line 4 <br>)
* - 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. <DIV><UL> in the case of the multiple LIs)
* - outerContectEnd: An HTML string that closes all necessary tags from the ancestor element (e.g. </UL></DIV>
* - innerContextStart: A string that opens all necessary tags between the ancestor
* and the beginning of the selection (e.g. <LI>)
* - innerContextEnd: A string that closes all tags after the end of the selection to the ancestor (e.g. </LI>)
* - 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);
};
});
}());

View File

@ -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 <a...><div></div></a> 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 = '&nbsp;'; // 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;
};
});
}());

View File

@ -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');
}
]);

View File

@ -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');
}
]);

View File

@ -227,9 +227,66 @@
<div class="details">
<div class="row">
<div class="col-sm-8">
<h3 translate>Text</h3>
<div ng-bind-html="motion.getText(version) | trusted"></div>
<div class="pull-right inline-editing-activator" ng-if="inlineEditing.allowed">
<button ng-if="!inlineEditing.active" ng-click="enableInlineEditing()" class="btn btn-sm btn-default">
<i class="fa fa-toggle-off"></i>
<translate>Inline editing inactive</translate>
</button>
<button ng-if="inlineEditing.active" ng-click="disableInlineEditing()" class="btn btn-sm btn-default">
<i class="fa fa-toggle-on"></i>
<translate>Inline editing active</translate>
</button>
</div>
<div class="line-number-setter {{ lineNumberMode }}">
<div class="btn-group" data-toggle="buttons">
<div class="btn btn-default disabled" title="{{ 'Line Numbering' | translate }}">
<i class="fa fa-list-ol" aria-hidden="true"></i>
</div>
<label class="btn btn-default" ng-class="{active: (lineNumberMode == 'none')}"
ng-click="lineNumberMode = 'none';">
<input type="radio" name="lineNumberMode" value="none" ng-model="lineNumberMode"
ng-checked="lineNumberMode == 'none'">
<translate>None</translate>
</label>
<label class="btn btn-default" ng-class="{active: (lineNumberMode == 'inline')}"
ng-click="lineNumberMode = 'inline'">
<input type="radio" name="lineNumberMode" value="inline" ng-model="lineNumberMode"
ng-checked="lineNumberMode == 'inline'">
<translate>Inline</translate>
</label>
<label class="btn btn-default" ng-class="{active: (lineNumberMode == 'outside')}"
ng-click="lineNumberMode = 'outside'">
<input type="radio" name="lineNumberMode" value="outside" ng-model="lineNumberMode"
ng-checked="lineNumberMode == 'outside'">
<translate>Outside</translate>
</label>
</div>
</div>
<div ng-class="{'col-sm-8': (lineNumberMode != 'outside'), 'col-sm-12': (lineNumberMode == 'outside')}">
<div ng-if="inlineEditing.allowed">
<div ng-show="inlineEditing.active">
<div ui-tinymce="tinymceOptions" ng-model="lineBrokenText"
class="motion-text line-numbers-{{ lineNumberMode }}"></div>
</div>
<div ng-show="!inlineEditing.active" ng-bind-html="motion.getTextWithLineBreaks(version) | trusted"
class="motion-text line-numbers-{{ lineNumberMode }}"></div>
<div class="motion-save-toolbar" ng-class="{ 'visible': (inlineEditing.changed && inlineEditing.active) }">
<div class="changed-hint" translate>The text has been changed.</div>
<button type="button" ng-click="motionInlineSave()" class="btn btn-primary">Save</button>
<label ng-if="inlineEditing.trivialChangeAllowed">
<input type="checkbox" ng-model="inlineEditing.trivialChange" value="1">
<span translate>Trivial change</span>
</label>
</div>
</div>
<div ng-if="!inlineEditing.allowed">
<div ng-bind-html="motion.getTextWithLineBreaks(version) | trusted"
class="motion-text line-numbers-{{ lineNumberMode }}"></div>
</div>
<!-- reason -->
<div ng-if="motion.getReason(version) != ''">
@ -286,6 +343,8 @@
<ul class="list-unstyled">
<li ng-repeat="message in motion.log_messages">
<small>{{ message.message }}</small>
</li>
</ul>
</div>
</div>
</div>

View File

@ -70,7 +70,8 @@
</div>
<!-- Text -->
<div ng-bind-html="motion.getText() | trusted"></div>
<div ng-bind-html="motion.getTextWithLineBreaks() | trusted"
class="motion-text line-numbers-{{ line_numbering.value }}"></div>
<!-- Reason -->
<h3 ng-if="motion.getReason()" translate>Reason</h3>

View File

@ -5,6 +5,7 @@
"prepublish": "bower install && gulp"
},
"devDependencies": {
"angular-mocks": "~1.5.7",
"bower": "~1.7.2",
"es6-promise": "~3.0.2",
"gulp": "~3.9.0",
@ -15,7 +16,11 @@
"gulp-jshint": "~2.0.0",
"gulp-rename": "~1.2.2",
"gulp-uglify": "~1.5.2",
"jasmine": "~2.4.1",
"jshint": "~2.9.2",
"karma": "~1.1.0",
"karma-jasmine": "~1.0.2",
"karma-chrome-launcher": "~1.0.1",
"main-bower-files": "~2.11.1",
"po2json": "~0.4.1",
"sprintf-js": "~1.0.3",

76
tests/karma/karma.conf.js Normal file
View File

@ -0,0 +1,76 @@
// Karma configuration
// Generated on Sun Jun 26 2016 14:46:31 GMT+0200 (CEST)
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '../..',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
files: [
'openslides/static/js/openslides-libs.js',
'node_modules/angular-mocks/angular-mocks.js',
'openslides/motions/static/js/motions/linenumbering.js',
'openslides/motions/static/js/motions/diff.js',
'tests/karma/*/*.test.js'
],
// list of files to exclude
exclude: [
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
client: {
captureConsole: true
},
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['Chrome'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: false,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity
})
}

View File

@ -0,0 +1,162 @@
describe('linenumbering', function () {
beforeEach(module('OpenSlidesApp.motions.diff'));
var diffService, baseHtmlDom1, baseHtmlDom2,
brMarkup = function (no) {
return '<br class="os-line-break">' +
'<span class="os-line-number line-number-' + no + '" data-line-number="' + no + '" contenteditable="false">&nbsp;</span>';
},
noMarkup = function (no) {
return '<span class="os-line-number line-number-' + no + '" data-line-number="' + no + '" contenteditable="false">&nbsp;</span>';
};
beforeEach(inject(function (_diffService_) {
diffService = _diffService_;
baseHtmlDom1 = diffService.htmlToFragment('<p>' +
noMarkup(1) + 'Line 1 ' + brMarkup(2) + 'Line 2' +
brMarkup(3) + 'Line <strong>3<br>' + noMarkup(4) + 'Line 4 ' + brMarkup(5) + 'Line</strong> 5</p>' +
'<ul class="ul-class">' +
'<li class="li-class">' + noMarkup(6) + 'Line 6 ' + brMarkup(7) + 'Line 7' + '</li>' +
'<li class="li-class"><ul>' +
'<li>' + noMarkup(8) + 'Level 2 LI 8</li>' +
'<li>' + noMarkup(9) + 'Level 2 LI 9</li>' +
'</ul></li>' +
'</ul>' +
'<p>' + noMarkup(10) + 'Line 10 ' + brMarkup(11) + 'Line 11</p>');
baseHtmlDom2 = diffService.htmlToFragment('<p><span class="os-line-number line-number-1" data-line-number="1" contenteditable="false">&nbsp;</span>Single text line</p>\
<p><span class="os-line-number line-number-2" data-line-number="2" contenteditable="false">&nbsp;</span>sdfsdfsdfsdf dsfsdfsdfdsflkewjrl ksjfl ksdjf&nbsp;klnlkjBavaria ipsum dolor sit amet Biazelt Auffisteign <br class="os-line-break"><span class="os-line-number line-number-3" data-line-number="3" contenteditable="false">&nbsp;</span>Schorsch mim Radl foahn Ohrwaschl Steckerleis wann griagd ma nacha wos zdringa glacht Mamalad, <br class="os-line-break">' +
'<span class="os-line-number line-number-4" data-line-number="4" contenteditable="false">&nbsp;</span>muass? I bin a woschechta Bayer sowos oamoi und sei und glei wirds no fui lustiga: Jo mei khkhis des <br class="os-line-break"><span class="os-line-number line-number-5" data-line-number="5" contenteditable="false">&nbsp;</span>schee middn ognudelt, Trachtnhuat Biawambn gscheid: Griasd eich midnand etza nix Gwiass woass ma ned <br class="os-line-break">' +
'<span class="os-line-number line-number-6" data-line-number="6" contenteditable="false">&nbsp;</span>owe. Dahoam gscheckate middn Spuiratz des is a gmahde Wiesn. Des is schee so Obazda san da, Haferl <br class="os-line-break"><span class="os-line-number line-number-7" data-line-number="7" contenteditable="false">&nbsp;</span>pfenningguat schoo griasd eich midnand.</p>\
<ul>\
<li><span class="os-line-number line-number-8" data-line-number="8" contenteditable="false">&nbsp;</span>Auffi Gamsbart nimma de Sepp Ledahosn Ohrwaschl um Godds wujn Wiesn Deandlgwand Mongdratzal! Jo <br class="os-line-break"><span class="os-line-number line-number-9" data-line-number="9" contenteditable="false">&nbsp;</span>leck mi Mamalad i daad mechad?</li>\
<li><span class="os-line-number line-number-10" data-line-number="10" contenteditable="false">&nbsp;</span>Do nackata Wurscht i hob di narrisch gean, Diandldrahn Deandlgwand vui huift vui woaß?</li>\
<li><span class="os-line-number line-number-11" data-line-number="11" contenteditable="false">&nbsp;</span>Ned Mamalad auffi i bin a woschechta Bayer greaßt eich nachad, umananda gwiss nia need <br class="os-line-break"><span class="os-line-number line-number-12" data-line-number="12" contenteditable="false">&nbsp;</span>Weiznglasl.</li>\
<li><span class="os-line-number line-number-13" data-line-number="13" contenteditable="false">&nbsp;</span>Woibbadinga noch da Giasinga Heiwog Biazelt mechad mim Spuiratz, soi zwoa.</li>\
</ul>\
<p><span class="os-line-number line-number-14" data-line-number="14" contenteditable="false">&nbsp;</span>I waar soweid Blosmusi es nomoi. Broadwurschtbudn des is a gmahde Wiesn Kirwa mogsd a Bussal <br class="os-line-break"><span class="os-line-number line-number-15" data-line-number="15" contenteditable="false">&nbsp;</span>Guglhupf schüds nei. Luja i moan oiwei Baamwach Watschnbaam, wiavui baddscher! Biakriagal a fescha <br class="os-line-break">' +
'<span class="os-line-number line-number-16" data-line-number="16" contenteditable="false">&nbsp;</span>1Bua Semmlkneedl iabaroi oba um Godds wujn Ledahosn wui Greichats. Geh um Godds wujn luja heid <br class="os-line-break"><span class="os-line-number line-number-17" data-line-number="17" contenteditable="false">&nbsp;</span>greaßt eich nachad woaß Breihaus eam! De om aufn Gipfe auf gehds beim Schichtl mehra Baamwach a <br class="os-line-break"><span class="os-line-number line-number-18" data-line-number="18" contenteditable="false">&nbsp;</span>bissal wos gehd ollaweil gscheid:</p>\
<blockquote>\
<p><span class="os-line-number line-number-19" data-line-number="19" contenteditable="false">&nbsp;</span>Scheans Schdarmbeaga See i hob di narrisch gean i jo mei is des schee! Nia eam <br class="os-line-break"><span class="os-line-number line-number-20" data-line-number="20" contenteditable="false">&nbsp;</span>hod vasteh i sog ja nix, i red ja bloß sammawiedaguad, umma eana obandeln! Zwoa <br class="os-line-break"><span class="os-line-number line-number-21" data-line-number="21" contenteditable="false">&nbsp;</span>jo mei scheans amoi, san und hoggd Milli barfuaßat gscheit. Foidweg vui huift <br class="os-line-break">' +
'<span class="os-line-number line-number-22" data-line-number="22" contenteditable="false">&nbsp;</span>vui singan, mehra Biakriagal om aufn Gipfe! Ozapfa sodala Charivari greaßt eich <br class="os-line-break"><span class="os-line-number line-number-23" data-line-number="23" contenteditable="false">&nbsp;</span>nachad Broadwurschtbudn do middn liberalitas Bavariae sowos Leonhardifahrt:</p>\
</blockquote>\
<p><span class="os-line-number line-number-24" data-line-number="24" contenteditable="false">&nbsp;</span>Wui helfgod Wiesn, ognudelt schaugn: Dahoam gelbe Rüam Schneid singan wo hi sauba i moan scho aa no <br class="os-line-break"><span class="os-line-number line-number-25" data-line-number="25" contenteditable="false">&nbsp;</span>a Maß a Maß und no a Maß nimma. Is umananda a ganze Hoiwe zwoa, Schneid. Vui huift vui Brodzeid kumm <br class="os-line-break">' +
'<span class="os-line-number line-number-26" data-line-number="26" contenteditable="false">&nbsp;</span>geh naa i daad vo de allerweil, gor. Woaß wia Gams, damischa. A ganze Hoiwe Ohrwaschl Greichats <br class="os-line-break"><span class="os-line-number line-number-27" data-line-number="27" contenteditable="false">&nbsp;</span>iabaroi Prosd Engelgwand nix Reiwadatschi.Weibaleid ognudelt Ledahosn noch da Giasinga Heiwog i daad <br class="os-line-break">' +
'<span class="os-line-number line-number-28" data-line-number="28" contenteditable="false">&nbsp;</span>Almrausch, Ewig und drei Dog nackata wea ko, dea ko. Meidromml Graudwiggal nois dei, nackata. No <br class="os-line-break"><span class="os-line-number line-number-29" data-line-number="29" contenteditable="false">&nbsp;</span>Diandldrahn nix Gwiass woass ma ned hod boarischer: Samma sammawiedaguad wos, i hoam Brodzeid. Jo <br class="os-line-break">' +
'<span class="os-line-number line-number-30" data-line-number="30" contenteditable="false">&nbsp;</span>mei Sepp Gaudi, is ma Wuascht do Hendl Xaver Prosd eana an a bravs. Sauwedda an Brezn, abfieseln.</p>');
diffService._insertInternalLineMarkers(baseHtmlDom1);
diffService._insertInternalLineMarkers(baseHtmlDom2);
}));
describe('extraction of lines', function () {
it('locates line number nodes', function() {
var lineNumberNode = diffService.getLineNumberNode(baseHtmlDom1, 4);
expect(lineNumberNode.parentNode.nodeName).toBe('STRONG');
lineNumberNode = diffService.getLineNumberNode(baseHtmlDom1, 9);
expect(lineNumberNode.parentNode.nodeName).toBe('UL');
lineNumberNode = diffService.getLineNumberNode(baseHtmlDom1, 15);
expect(lineNumberNode).toBe(null);
});
it('finds the common ancestor', function() {
var fromLineNode, toLineNode, commonAncestor;
fromLineNode = diffService.getLineNumberNode(baseHtmlDom1, 6);
toLineNode = diffService.getLineNumberNode(baseHtmlDom1, 7);
commonAncestor = diffService._getCommonAncestor(fromLineNode, toLineNode);
expect(commonAncestor.commonAncestor.nodeName).toBe("#document-fragment");
fromLineNode = diffService.getLineNumberNode(baseHtmlDom1, 6);
toLineNode = diffService.getLineNumberNode(baseHtmlDom1, 8);
commonAncestor = diffService._getCommonAncestor(fromLineNode, toLineNode);
expect(commonAncestor.commonAncestor.nodeName).toBe("#document-fragment");
fromLineNode = diffService.getLineNumberNode(baseHtmlDom1, 6);
toLineNode = diffService.getLineNumberNode(baseHtmlDom1, 10);
commonAncestor = diffService._getCommonAncestor(fromLineNode, toLineNode);
expect(commonAncestor.commonAncestor.nodeName).toBe("#document-fragment");
});
it('renders DOMs correctly (1)', function() {
var lineNo = diffService.getLineNumberNode(baseHtmlDom1, 7),
greatParent = lineNo.parentNode.parentNode,
lineTrace = [lineNo.parentNode, lineNo];
var pre = diffService._serializePartialDomToChild(greatParent, lineTrace, true);
expect(pre).toBe('<UL class="ul-class"><LI class="li-class">Line 6 ');
lineTrace = [lineNo.parentNode, lineNo];
var post = diffService._serializePartialDomFromChild(greatParent, lineTrace, true);
expect(post).toBe('Line 7' + '</LI>' +
'<LI class="li-class"><UL>' +
'<LI>Level 2 LI 8</LI>' +
'<LI>Level 2 LI 9</LI>' +
'</UL></LI>' +
'</UL>');
});
it('renders DOMs correctly (2)', function() {
var lineNo = diffService.getLineNumberNode(baseHtmlDom1, 9),
greatParent = lineNo.parentNode.parentNode,
lineTrace = [lineNo.parentNode, lineNo];
var pre = diffService._serializePartialDomToChild(greatParent, lineTrace, true);
expect(pre).toBe('<LI class="li-class"><UL><LI>Level 2 LI 8</LI>');
});
it('extracts a single line', function () {
var diff = diffService.extractRangeByLineNumbers(baseHtmlDom1, 1, 2);
expect(diff.html).toBe('<P>Line 1 ');
expect(diff.outerContextStart).toBe('');
expect(diff.outerContextEnd).toBe('');
});
it('extracts lines from nested UL/LI-structures', function () {
var diff = diffService.extractRangeByLineNumbers(baseHtmlDom1, 7, 9);
expect(diff.html).toBe('Line 7</LI><LI class="li-class"><UL><LI>Level 2 LI 8</LI>');
expect(diff.ancestor.nodeName).toBe('UL');
expect(diff.outerContextStart).toBe('<UL class="ul-class">');
expect(diff.outerContextEnd).toBe('</UL>');
expect(diff.innerContextStart).toBe('<LI class="li-class">');
expect(diff.innerContextEnd).toBe('</UL></LI>');
expect(diff.previousHtmlEndSnippet).toBe('</LI></UL>');
expect(diff.followingHtmlStartSnippet).toBe('<UL class="ul-class"><LI class="li-class"><UL>');
});
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.</P><UL><LI>Auffi Gamsbart nimma de Sepp Ledahosn Ohrwaschl um Godds wujn Wiesn Deandlgwand Mongdratzal! Jo leck mi Mamalad i daad mechad?</LI><LI>Do nackata Wurscht i hob di narrisch gean, Diandldrahn Deandlgwand vui huift vui woaß?</LI>');
expect(diff.ancestor.nodeName).toBe('#document-fragment');
expect(diff.outerContextStart).toBe('');
expect(diff.outerContextEnd).toBe('');
expect(diff.innerContextStart).toBe('<P>');
expect(diff.innerContextEnd).toBe('</UL>');
expect(diff.previousHtmlEndSnippet).toBe('</P>');
expect(diff.followingHtmlStartSnippet).toBe('<UL>');
});
});
describe('merging lines into the original motion', function () {
it('replaces LIs by a P', function () {
var merged = diffService.replaceLines(baseHtmlDom1, '<p>Replaced a UL by a P</p>', 6, 9);
expect(merged).toBe('<P>Line 1 Line 2Line <STRONG>3<BR>Line 4 Line</STRONG> 5</P><P>Replaced a UL by a P</P><UL class="ul-class"><LI class="li-class"><UL><LI>Level 2 LI 9</LI></UL></LI></UL><P>Line 10 Line 11</P>');
});
/*
it('replaces LIs by another LI', function () {
var merged = diffService.replaceLines(baseHtmlDom1, '<UL class="ul-class"><LI>A new LI</LI></UL>', 6, 9);
expect(merged).toBe('');
});
*/
});
});

View File

@ -0,0 +1,216 @@
describe('linenumbering', function () {
beforeEach(module('OpenSlidesApp.motions.lineNumbering'));
var lineNumberingService,
brMarkup = function (no) {
return '<br class="os-line-break">' +
'<span class="os-line-number line-number-' + no + '" data-line-number="' + no + '" contenteditable="false">&nbsp;</span>';
},
noMarkup = function (no) {
return '<span class="os-line-number line-number-' + no + '" data-line-number="' + no + '" contenteditable="false">&nbsp;</span>';
},
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 = "<span>Test</span>";
var outHtml = lineNumberingService.insertLineNumbers(inHtml, 5);
expect(outHtml).toBe(noMarkup(1) + '<span>Test</span>');
expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml);
});
it('breaks lines in a simple SPAN', function () {
var inHtml = "<span>Lorem ipsum dolorsit amet</span>";
var outHtml = lineNumberingService.insertLineNumbers(inHtml, 5);
expect(outHtml).toBe(noMarkup(1) + '<span>Lorem ' + brMarkup(2) + 'ipsum ' + brMarkup(3) + 'dolor' + brMarkup(4) + 'sit ' + brMarkup(5) + 'amet</span>');
expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml);
});
it('breaks lines in nested inline elements', function () {
var inHtml = "<span>Lorem <strong>ipsum dolorsit</strong> amet</span>";
var outHtml = lineNumberingService.insertLineNumbers(inHtml, 5);
expect(outHtml).toBe(noMarkup(1) + '<span>Lorem ' + brMarkup(2) + '<strong>ipsum ' + brMarkup(3) + 'dolor' + brMarkup(4) + 'sit</strong> ' + brMarkup(5) + 'amet</span>');
expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml);
});
});
describe('line numbering: block nodes', function () {
it('leaves a simple DIV untouched', function () {
var inHtml = "<div>Test</div>";
var outHtml = lineNumberingService.insertLineNumbers(inHtml, 5);
expect(outHtml).toBe('<div>' + noMarkup(1) + 'Test</div>');
expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml);
});
it('breaks a DIV containing only inline elements', function () {
var inHtml = "<div>Test <span>Test1234</span>5678 Test</div>";
var outHtml = lineNumberingService.insertLineNumbers(inHtml, 5);
expect(outHtml).toBe('<div>' + noMarkup(1) + 'Test ' + brMarkup(2) + '<span>Test1' + brMarkup(3) + '234</span>56' + brMarkup(4) + '78 ' + brMarkup(5) + 'Test</div>');
expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml);
});
it('handles a DIV within a DIV correctly', function () {
var inHtml = "<div>Te<div>Te Test</div>Test</div>";
var outHtml = lineNumberingService.insertLineNumbers(inHtml, 5);
expect(outHtml).toBe('<div>' + noMarkup(1) + 'Te<div>' + noMarkup(2) + 'Te ' + brMarkup(3) + 'Test</div>' + noMarkup(4) + 'Test</div>');
expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml);
});
it('ignores white spaces between block element tags', function () {
var inHtml = "<ul>\n<li>Test</li>\n</ul>";
var outHtml = lineNumberingService.insertLineNumbers(inHtml, 80);
expect(outHtml).toBe("<ul>\n<li>" + noMarkup(1) + 'Test</li>\n</ul>');
expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml);
});
});
describe('indentation for block elements', function () {
it('indents LI-elements', function () {
var inHtml = '<div>' +longstr(100) + '<ul><li>' + longstr(100) + '</li></ul>' + longstr(100) + '</div>';
var expected = '<div>' + noMarkup(1) +
'ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZAB' + brMarkup(2) + 'CDEFGHIJKLMNOPQRSTUV' +
'<ul><li>' + noMarkup(3) +
'ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVW' + brMarkup(4) + 'XYZABCDEFGHIJKLMNOPQRSTUV' +
'</li></ul>' + noMarkup(5) +
'ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZAB' + brMarkup(6) + 'CDEFGHIJKLMNOPQRSTUV</div>';
var outHtml = lineNumberingService.insertLineNumbers(inHtml, 80);
expect(outHtml).toBe(expected);
expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml);
});
it('indents BLOCKQUOTE-elements', function () {
var inHtml = '<div>' +longstr(100) + '<blockquote>' + longstr(100) + '</blockquote>' + longstr(100) + '</div>';
var expected = '<div>' + noMarkup(1) +
'ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZAB' + brMarkup(2) + 'CDEFGHIJKLMNOPQRSTUV' +
'<blockquote>' + noMarkup(3) +
'ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGH' + brMarkup(4) + 'IJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUV' +
'</blockquote>' + noMarkup(5) +
'ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZAB' + brMarkup(6) + 'CDEFGHIJKLMNOPQRSTUV</div>';
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 = '<h1>' + longstr(80) + '</h1>';
var expected = '<h1>' + noMarkup(1) + 'ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMN' +
brMarkup(2) + 'OPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZAB</h1>';
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 = '<h2>' + longstr(80) + '</h2>';
var expected = '<h2>' + noMarkup(1) + 'ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZA' +
brMarkup(2) + 'BCDEFGHIJKLMNOPQRSTUVWXYZAB</h2>';
var outHtml = lineNumberingService.insertLineNumbers(inHtml, 80);
expect(outHtml).toBe(expected);
expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml);
});
it('indents Ps with 30px-padding by 6 characters', function () {
var inHtml = '<div style="padding-left: 30px;">' + longstr(80) + '</div>';
var expected = '<div style="padding-left: 30px;">' + noMarkup(1) + 'ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUV' +
brMarkup(2) + 'WXYZAB</div>';
var outHtml = lineNumberingService.insertLineNumbers(inHtml, 80);
expect(outHtml).toBe(expected);
expect(lineNumberingService.stripLineNumbers(outHtml)).toBe(inHtml);
});
});
});