Line numbers and inline editing
This commit is contained in:
parent
5ea9cbf4d1
commit
d986d551e0
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
},
|
||||
|
407
openslides/motions/static/js/motions/diff.js
Normal file
407
openslides/motions/static/js/motions/diff.js
Normal 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);
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
}());
|
329
openslides/motions/static/js/motions/linenumbering.js
Normal file
329
openslides/motions/static/js/motions/linenumbering.js
Normal 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 = ' '; // 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;
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
}());
|
@ -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');
|
||||
}
|
||||
]);
|
||||
|
||||
|
@ -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');
|
||||
}
|
||||
]);
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
76
tests/karma/karma.conf.js
Normal 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
|
||||
})
|
||||
}
|
162
tests/karma/motions/diff.service.test.js
Normal file
162
tests/karma/motions/diff.service.test.js
Normal 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"> </span>';
|
||||
},
|
||||
noMarkup = function (no) {
|
||||
return '<span class="os-line-number line-number-' + no + '" data-line-number="' + no + '" contenteditable="false"> </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"> </span>Single text line</p>\
|
||||
<p><span class="os-line-number line-number-2" data-line-number="2" contenteditable="false"> </span>sdfsdfsdfsdf dsfsdfsdfdsflkewjrl ksjfl ksdjf 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"> </span>Schorsch mim Radl foahn Ohrwaschl Steckerleis wann griagd ma nacha wos z’dringa glacht Mamalad, <br class="os-line-break">' +
|
||||
'<span class="os-line-number line-number-4" data-line-number="4" contenteditable="false"> </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"> </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"> </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"> </span>pfenningguat schoo griasd eich midnand.</p>\
|
||||
<ul>\
|
||||
<li><span class="os-line-number line-number-8" data-line-number="8" contenteditable="false"> </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"> </span>leck mi Mamalad i daad mechad?</li>\
|
||||
<li><span class="os-line-number line-number-10" data-line-number="10" contenteditable="false"> </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"> </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"> </span>Weiznglasl.</li>\
|
||||
<li><span class="os-line-number line-number-13" data-line-number="13" contenteditable="false"> </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"> </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"> </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"> </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"> </span>greaßt eich nachad woaß Breihaus eam! De om auf’n 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"> </span>bissal wos gehd ollaweil gscheid:</p>\
|
||||
<blockquote>\
|
||||
<p><span class="os-line-number line-number-19" data-line-number="19" contenteditable="false"> </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"> </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"> </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"> </span>vui singan, mehra Biakriagal om auf’n 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"> </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"> </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"> </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"> </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"> </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"> </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"> </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"> </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('');
|
||||
});
|
||||
*/
|
||||
|
||||
});
|
||||
});
|
216
tests/karma/motions/linenumbering.service.test.js
Normal file
216
tests/karma/motions/linenumbering.service.test.js
Normal 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"> </span>';
|
||||
},
|
||||
noMarkup = function (no) {
|
||||
return '<span class="os-line-number line-number-' + no + '" data-line-number="' + no + '" contenteditable="false"> </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);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user