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 -
+ + +
+ + + + + + +
+
-
@@ -319,7 +339,7 @@
-
@@ -332,7 +352,7 @@
-
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 @@ -