diff --git a/openslides/core/static/css/app.css b/openslides/core/static/css/app.css
index c4d246341..332a6d87d 100644
--- a/openslides/core/static/css/app.css
+++ b/openslides/core/static/css/app.css
@@ -308,6 +308,11 @@ img {
margin-left: 15px;
}
+.col1 .details .line-number-setter > span {
+ margin-right: 5px;
+ float: left;
+}
+
.col1 .details .line-number-setter .btn.disabled {
cursor: default;
opacity: 1;
@@ -381,6 +386,10 @@ img {
}
/*** Line numbers ***/
+.motion-text .highlight {
+ background-color: #ff0;
+}
+
.motion-text.line-numbers-outside {
padding-left: 35px;
position: relative;
@@ -810,6 +819,11 @@ img {
padding-right: 4px !important;
}
+.btn-slim {
+ padding-left: 6px;
+ padding-right: 6px;
+}
+
.spacer, .spacer-top {
margin-top: 7px;
}
diff --git a/openslides/core/static/css/projector.css b/openslides/core/static/css/projector.css
index ac46756ac..2b2d5c4d3 100644
--- a/openslides/core/static/css/projector.css
+++ b/openslides/core/static/css/projector.css
@@ -381,6 +381,10 @@ tr.elected td {
/*** Line numbers ***/
+.motion-text .highlight {
+ background-color: #ff0;
+}
+
.motion-text.line-numbers-outside {
padding-left: 0;
margin-left: 25px;
@@ -424,5 +428,5 @@ tr.elected td {
display: none;
}
.motion-text.line-numbers-none .os-line-number {
- display: none;
+ visibility: hidden;
}
diff --git a/openslides/core/static/js/core/projector.js b/openslides/core/static/js/core/projector.js
index daceaf745..af42559ca 100644
--- a/openslides/core/static/js/core/projector.js
+++ b/openslides/core/static/js/core/projector.js
@@ -144,7 +144,7 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
}
});
// TODO: Use the current projector. At the moment there is only one
- $scope.scroll = -5 * Projector.get(1).scroll;
+ $scope.scroll = -80 * Projector.get(1).scroll;
$scope.scale = 100 + 20 * Projector.get(1).scale;
}
});
diff --git a/openslides/core/static/templates/projector.html b/openslides/core/static/templates/projector.html
index 12aa50275..c8f24b630 100644
--- a/openslides/core/static/templates/projector.html
+++ b/openslides/core/static/templates/projector.html
@@ -37,7 +37,7 @@
diff --git a/openslides/core/views.py b/openslides/core/views.py
index dcef2f0bf..0c3ea6ef4 100644
--- a/openslides/core/views.py
+++ b/openslides/core/views.py
@@ -192,7 +192,8 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
elif self.action == 'metadata':
result = self.request.user.has_perm('core.can_see_projector')
elif self.action in ('activate_elements', 'prune_elements', 'update_elements',
- 'deactivate_elements', 'clear_elements', 'control_view', 'set_resolution'):
+ 'deactivate_elements', 'clear_elements', 'control_view',
+ 'set_resolution', 'set_scroll'):
result = (self.request.user.has_perm('core.can_see_projector') and
self.request.user.has_perm('core.can_manage_projector'))
else:
@@ -428,6 +429,25 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
direction=request.data['direction'])
return Response({'detail': message})
+ @detail_route(methods=['post'])
+ def set_scroll(self, request, pk):
+ """
+ REST API operation to scroll the projector.
+
+ It expects a POST request to
+ /rest/core/projector/
/set_scroll/ with a new value for scroll.
+ """
+ if not isinstance(request.data, int):
+ raise ValidationError({'detail': 'Data must be an int.'})
+
+ projector_instance = self.get_object()
+ projector_instance.scroll = request.data
+
+ projector_instance.save()
+ message = 'Setting scroll to {scroll} was successful.'.format(
+ scroll=request.data)
+ return Response({'detail': message})
+
class CustomSlideViewSet(ModelViewSet):
"""
diff --git a/openslides/motions/static/js/motions/base.js b/openslides/motions/static/js/motions/base.js
index 5d1bf6a0c..46c7134e9 100644
--- a/openslides/motions/static/js/motions/base.js
+++ b/openslides/motions/static/js/motions/base.js
@@ -162,11 +162,11 @@ angular.module('OpenSlidesApp.motions', [
getText: function (versionId) {
return this.getVersion(versionId).text;
},
- getTextWithLineBreaks: function (versionId) {
+ getTextWithLineBreaks: function (versionId, highlight, callback) {
var lineLength = Config.get('motions_line_length').value,
html = this.getVersion(versionId).text;
- return lineNumberingService.insertLineNumbers(html, lineLength);
+ return lineNumberingService.insertLineNumbers(html, lineLength, highlight, callback);
},
setTextStrippingLineBreaks: function (versionId, text) {
this.text = lineNumberingService.stripLineNumbers(text);
diff --git a/openslides/motions/static/js/motions/linenumbering.js b/openslides/motions/static/js/motions/linenumbering.js
index a09c2bd44..518684c84 100644
--- a/openslides/motions/static/js/motions/linenumbering.js
+++ b/openslides/motions/static/js/motions/linenumbering.js
@@ -63,6 +63,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
this._currentLineNumber++;
node.setAttribute('class', 'os-line-number line-number-' + lineNumber);
node.setAttribute('data-line-number', lineNumber + '');
+ node.setAttribute('name', 'L' + lineNumber);
node.setAttribute('contenteditable', 'false');
node.innerHTML = ' '; // Prevent tinymce from stripping out empty span's
return node;
@@ -78,23 +79,36 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
* @returns Array
* @private
*/
- this._textNodeToLines = function (node, length) {
+ this._textNodeToLines = function (node, length, highlight) {
var out = [],
currLineStart = 0,
i = 0,
firstTextNode = true,
lastBreakableIndex = null,
service = this;
-
- var addLine = function (text) {
- var newNode = document.createTextNode(text);
+ var addLine = function (text, highlight) {
+ var node;
if (firstTextNode) {
+ if (highlight == service._currentLineNumber - 1) {
+ node = document.createElement('span');
+ node.setAttribute('class', 'highlight');
+ node.innerHTML = text;
+ } else {
+ node = document.createTextNode(text);
+ }
firstTextNode = false;
} else {
+ if (service._currentLineNumber == highlight) {
+ node = document.createElement('span');
+ node.setAttribute('class', 'highlight');
+ node.innerHTML = text;
+ } else {
+ node = document.createTextNode(text);
+ }
out.push(service._createLineBreak());
out.push(service._createLineNumber());
}
- out.push(newNode);
+ out.push(node);
};
if (node.nodeValue == "\n") {
@@ -122,7 +136,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
}
if (lineBreakAt !== null && node.nodeValue[i] != ' ') {
var currLine = node.nodeValue.substring(currLineStart, lineBreakAt + 1);
- addLine(currLine);
+ addLine(currLine, highlight);
currLineStart = lineBreakAt + 1;
this._currentInlineOffset = i - lineBreakAt - 1;
@@ -137,7 +151,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
i++;
}
- addLine(node.nodeValue.substring(currLineStart));
+ addLine(node.nodeValue.substring(currLineStart), highlight);
}
return out;
};
@@ -177,7 +191,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
}
};
- this._insertLineNumbersToInlineNode = function (node, length) {
+ this._insertLineNumbersToInlineNode = function (node, length, highlight) {
var oldChildren = [], i;
for (i = 0; i < node.childNodes.length; i++) {
oldChildren.push(node.childNodes[i]);
@@ -189,7 +203,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
for (i = 0; i < oldChildren.length; i++) {
if (oldChildren[i].nodeType == TEXT_NODE) {
- var ret = this._textNodeToLines(oldChildren[i], length);
+ var ret = this._textNodeToLines(oldChildren[i], length, highlight);
for (var j = 0; j < ret.length; j++) {
node.appendChild(ret[j]);
}
@@ -201,8 +215,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
node.appendChild(this._createLineBreak());
node.appendChild(this._createLineNumber());
}
-
- var changedNode = this._insertLineNumbersToNode(oldChildren[i], length);
+ var changedNode = this._insertLineNumbersToNode(oldChildren[i], length, highlight);
this._moveLeadingLineBreaksToOuterNode(changedNode, node);
node.appendChild(changedNode);
} else {
@@ -253,7 +266,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
return Math.ceil(newLength);
};
- this._insertLineNumbersToBlockNode = function (node, length) {
+ this._insertLineNumbersToBlockNode = function (node, length, highlight) {
this._currentInlineOffset = 0;
this._prependLineNumberToFirstText = true;
@@ -268,7 +281,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
for (i = 0; i < oldChildren.length; i++) {
if (oldChildren[i].nodeType == TEXT_NODE) {
- var ret = this._textNodeToLines(oldChildren[i], length);
+ var ret = this._textNodeToLines(oldChildren[i], length, highlight);
for (var j = 0; j < ret.length; j++) {
node.appendChild(ret[j]);
}
@@ -280,8 +293,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
node.appendChild(this._createLineBreak());
node.appendChild(this._createLineNumber());
}
-
- var changedNode = this._insertLineNumbersToNode(oldChildren[i], length);
+ var changedNode = this._insertLineNumbersToNode(oldChildren[i], length, highlight);
this._moveLeadingLineBreaksToOuterNode(changedNode, node);
node.appendChild(changedNode);
} else {
@@ -295,15 +307,15 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
return node;
};
- this._insertLineNumbersToNode = function (node, length) {
+ this._insertLineNumbersToNode = function (node, length, highlight) {
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);
+ return this._insertLineNumbersToInlineNode(node, length, highlight);
} else {
var newLength = this._calcBlockNodeLength(node, length);
- return this._insertLineNumbersToBlockNode(node, newLength);
+ return this._insertLineNumbersToBlockNode(node, newLength, highlight);
}
};
@@ -329,7 +341,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
return root.innerHTML;
};
- this.insertLineNumbersNode = function (html, lineLength) {
+ this.insertLineNumbersNode = function (html, lineLength, highlight) {
var root = document.createElement('div');
root.innerHTML = html;
@@ -337,11 +349,15 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
this._currentLineNumber = 1;
this._prependLineNumberToFirstText = true;
- return this._insertLineNumbersToNode(root, lineLength);
+ return this._insertLineNumbersToNode(root, lineLength, highlight);
};
- this.insertLineNumbers = function (html, lineLength) {
- var newRoot = this.insertLineNumbersNode(html, lineLength);
+ this.insertLineNumbers = function (html, lineLength, highlight, callback) {
+ var newRoot = this.insertLineNumbersNode(html, lineLength, highlight);
+
+ if (callback) {
+ callback();
+ }
return newRoot.innerHTML;
};
diff --git a/openslides/motions/static/js/motions/projector.js b/openslides/motions/static/js/motions/projector.js
index 31234c264..4b2b5a8d7 100644
--- a/openslides/motions/static/js/motions/projector.js
+++ b/openslides/motions/static/js/motions/projector.js
@@ -15,14 +15,69 @@ angular.module('OpenSlidesApp.motions.projector', ['OpenSlidesApp.motions'])
.controller('SlideMotionCtrl', [
'$scope',
+ '$rootScope',
+ '$http',
'Motion',
'User',
'Config',
- function($scope, Motion, User, Config) {
+ 'Projector',
+ function($scope, $rootScope, $http, Motion, User, Config, Projector) {
// 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.
var id = $scope.element.id;
+
+ $scope.line = $scope.element.highlightAndScroll;
+
+ // get cookie using jQuery
+ var getCookie = function (name) {
+ var cookieValue = null;
+ if (document.cookie && document.cookie !== '') {
+ var cookies = document.cookie.split(';');
+ for (var i = 0; i < cookies.length; i++) {
+ var cookie = jQuery.trim(cookies[i]);
+ // Does this cookie string begin with the name we want?
+ if (cookie.substring(0, name.length + 1) == (name + '=')) {
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+ break;
+ }
+ }
+ }
+ return cookieValue;
+ };
+
+ var scrollRequest = function (position) {
+ // request with csrf token
+ // TODO: Why is the X-CSRFToken not included in the header by default?
+ var csrfToken = getCookie('csrftoken');
+ var request = {
+ method: 'POST',
+ url: '/rest/core/projector/1/set_scroll/',
+ data: position,
+ headers: {
+ 'X-CSRFToken': csrfToken
+ }
+ };
+ $http(request);
+ };
+ $scope.scroll = function () {
+ // Prevent getting in an infinite loop by updating only if the value has changed.
+ // (if this check is removed this happends: controller loads --> call of $scope.scroll
+ // --> same line but scrollRequest --> projector updates --> controller loads --> ... )
+ if ($scope.line !== $rootScope.motion_projector_line) {
+ // line value has changed
+ var lineElement = document.getElementsByName('L' + $scope.line);
+ if (lineElement[0]) {
+ $rootScope.motion_projector_line = $scope.line;
+ var pos = lineElement[0].getBoundingClientRect().top + Projector.get(1).scroll*80;
+ scrollRequest(Math.floor(pos/80.0) - 1);
+ } else if ($scope.line === 0) {
+ $rootScope.motion_projector_line = $scope.line;
+ scrollRequest(0);
+ }
+ }
+ };
+
Motion.bindOne(id, $scope, 'motion');
User.bindAll({}, $scope, 'users');
}
diff --git a/openslides/motions/static/js/motions/site.js b/openslides/motions/static/js/motions/site.js
index 9106c7d3f..6cc5693fe 100644
--- a/openslides/motions/static/js/motions/site.js
+++ b/openslides/motions/static/js/motions/site.js
@@ -1069,9 +1069,10 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
'PdfMakeDocumentProvider',
'MotionInlineEditing',
'gettextCatalog',
+ 'Projector',
function($scope, $http, ngDialog, MotionComment, MotionForm, Motion, Category, Mediafile, Tag,
User, Workflow, Config, motion, SingleMotionContentProvider, MotionContentProvider,
- PollContentProvider, PdfMakeConverter, PdfMakeDocumentProvider, MotionInlineEditing, gettextCatalog) {
+ PollContentProvider, PdfMakeConverter, PdfMakeDocumentProvider, MotionInlineEditing, gettextCatalog, Projector) {
Motion.bindOne(motion.id, $scope, 'motion');
Category.bindAll({}, $scope, 'categories');
Mediafile.bindAll({}, $scope, 'mediafiles');
@@ -1088,6 +1089,38 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
}
$scope.amendments = Motion.filter({parent_id: motion.id});
+ $scope.highlight = 0;
+ $scope.linesForProjector = false;
+ // Set 0 for disable highlighting on projector
+ var setHighlightOnProjector = function (line) {
+ var elements = _.map(Projector.get(1).elements, function(element) { return element; });
+ elements.forEach(function (element) {
+ if (element.name == 'motions/motion') {
+ var data = {};
+ data[element.uuid] = {
+ highlightAndScroll: line,
+ };
+ $http.post('/rest/core/projector/1/update_elements/', data);
+ }
+ });
+ };
+ $scope.scrollToAndHighlight = function (line) {
+ $scope.highlight = line;
+ var lineElement = document.getElementsByName('L' + line);
+ if (lineElement[0]) {
+ // Scroll local
+ $('html, body').animate({
+ scrollTop: lineElement[0].getBoundingClientRect().top
+ }, 1000);
+ }
+ // set highlight and scroll on Projector
+ setHighlightOnProjector($scope.linesForProjector ? line : 0);
+ };
+ $scope.toggleLinesForProjector = function () {
+ $scope.linesForProjector = !$scope.linesForProjector;
+ setHighlightOnProjector($scope.linesForProjector ? $scope.highlight : 0);
+ };
+
$scope.makePDF = function() {
var id = motion.identifier,
slice = Function.prototype.call.bind([].slice),
diff --git a/openslides/motions/static/templates/motions/motion-detail.html b/openslides/motions/static/templates/motions/motion-detail.html
index d6064fcca..fcbcec778 100644
--- a/openslides/motions/static/templates/motions/motion-detail.html
+++ b/openslides/motions/static/templates/motions/motion-detail.html
@@ -287,7 +287,7 @@
-
+
@@ -309,9 +309,29 @@
ng-checked="lineNumberMode == 'outside'">
Outside
-
+
+
+
+
-
diff --git a/openslides/motions/static/templates/motions/slide_motion.html b/openslides/motions/static/templates/motions/slide_motion.html
index d974cabe8..7e5a13d29 100644
--- a/openslides/motions/static/templates/motions/slide_motion.html
+++ b/openslides/motions/static/templates/motions/slide_motion.html
@@ -70,7 +70,7 @@
-