Highlighting and jumping to lines in motions (closes #2347)

This commit is contained in:
Finn Stutzenstein 2016-09-05 16:23:44 +02:00 committed by FinnStutzenstein
parent 2d15bd54a1
commit a06806c33b
11 changed files with 198 additions and 36 deletions

View File

@ -308,6 +308,11 @@ img {
margin-left: 15px; margin-left: 15px;
} }
.col1 .details .line-number-setter > span {
margin-right: 5px;
float: left;
}
.col1 .details .line-number-setter .btn.disabled { .col1 .details .line-number-setter .btn.disabled {
cursor: default; cursor: default;
opacity: 1; opacity: 1;
@ -381,6 +386,10 @@ img {
} }
/*** Line numbers ***/ /*** Line numbers ***/
.motion-text .highlight {
background-color: #ff0;
}
.motion-text.line-numbers-outside { .motion-text.line-numbers-outside {
padding-left: 35px; padding-left: 35px;
position: relative; position: relative;
@ -810,6 +819,11 @@ img {
padding-right: 4px !important; padding-right: 4px !important;
} }
.btn-slim {
padding-left: 6px;
padding-right: 6px;
}
.spacer, .spacer-top { .spacer, .spacer-top {
margin-top: 7px; margin-top: 7px;
} }

View File

@ -381,6 +381,10 @@ tr.elected td {
/*** Line numbers ***/ /*** Line numbers ***/
.motion-text .highlight {
background-color: #ff0;
}
.motion-text.line-numbers-outside { .motion-text.line-numbers-outside {
padding-left: 0; padding-left: 0;
margin-left: 25px; margin-left: 25px;
@ -424,5 +428,5 @@ tr.elected td {
display: none; display: none;
} }
.motion-text.line-numbers-none .os-line-number { .motion-text.line-numbers-none .os-line-number {
display: none; visibility: hidden;
} }

View File

@ -144,7 +144,7 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
} }
}); });
// TODO: Use the current projector. At the moment there is only one // 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; $scope.scale = 100 + 20 * Projector.get(1).scale;
} }
}); });

View File

@ -37,7 +37,7 @@
<div ng-controller="ProjectorCtrl"> <div ng-controller="ProjectorCtrl">
<style type="text/css"> <style type="text/css">
.scrollcontent { .scrollcontent {
margin-top: {{scroll}}em !important; margin-top: {{scroll}}px !important;
font-size: {{scale}}%; font-size: {{scale}}%;
} }
</style> </style>

View File

@ -192,7 +192,8 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
elif self.action == 'metadata': elif self.action == 'metadata':
result = self.request.user.has_perm('core.can_see_projector') result = self.request.user.has_perm('core.can_see_projector')
elif self.action in ('activate_elements', 'prune_elements', 'update_elements', 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 result = (self.request.user.has_perm('core.can_see_projector') and
self.request.user.has_perm('core.can_manage_projector')) self.request.user.has_perm('core.can_manage_projector'))
else: else:
@ -428,6 +429,25 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
direction=request.data['direction']) direction=request.data['direction'])
return Response({'detail': message}) 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/<pk>/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): class CustomSlideViewSet(ModelViewSet):
""" """

View File

@ -162,11 +162,11 @@ angular.module('OpenSlidesApp.motions', [
getText: function (versionId) { getText: function (versionId) {
return this.getVersion(versionId).text; return this.getVersion(versionId).text;
}, },
getTextWithLineBreaks: function (versionId) { getTextWithLineBreaks: function (versionId, highlight, callback) {
var lineLength = Config.get('motions_line_length').value, var lineLength = Config.get('motions_line_length').value,
html = this.getVersion(versionId).text; html = this.getVersion(versionId).text;
return lineNumberingService.insertLineNumbers(html, lineLength); return lineNumberingService.insertLineNumbers(html, lineLength, highlight, callback);
}, },
setTextStrippingLineBreaks: function (versionId, text) { setTextStrippingLineBreaks: function (versionId, text) {
this.text = lineNumberingService.stripLineNumbers(text); this.text = lineNumberingService.stripLineNumbers(text);

View File

@ -63,6 +63,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
this._currentLineNumber++; this._currentLineNumber++;
node.setAttribute('class', 'os-line-number line-number-' + lineNumber); node.setAttribute('class', 'os-line-number line-number-' + lineNumber);
node.setAttribute('data-line-number', lineNumber + ''); node.setAttribute('data-line-number', lineNumber + '');
node.setAttribute('name', 'L' + lineNumber);
node.setAttribute('contenteditable', 'false'); node.setAttribute('contenteditable', 'false');
node.innerHTML = '&nbsp;'; // Prevent tinymce from stripping out empty span's node.innerHTML = '&nbsp;'; // Prevent tinymce from stripping out empty span's
return node; return node;
@ -78,23 +79,36 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
* @returns Array * @returns Array
* @private * @private
*/ */
this._textNodeToLines = function (node, length) { this._textNodeToLines = function (node, length, highlight) {
var out = [], var out = [],
currLineStart = 0, currLineStart = 0,
i = 0, i = 0,
firstTextNode = true, firstTextNode = true,
lastBreakableIndex = null, lastBreakableIndex = null,
service = this; service = this;
var addLine = function (text, highlight) {
var addLine = function (text) { var node;
var newNode = document.createTextNode(text);
if (firstTextNode) { 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; firstTextNode = false;
} else { } 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._createLineBreak());
out.push(service._createLineNumber()); out.push(service._createLineNumber());
} }
out.push(newNode); out.push(node);
}; };
if (node.nodeValue == "\n") { if (node.nodeValue == "\n") {
@ -122,7 +136,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
} }
if (lineBreakAt !== null && node.nodeValue[i] != ' ') { if (lineBreakAt !== null && node.nodeValue[i] != ' ') {
var currLine = node.nodeValue.substring(currLineStart, lineBreakAt + 1); var currLine = node.nodeValue.substring(currLineStart, lineBreakAt + 1);
addLine(currLine); addLine(currLine, highlight);
currLineStart = lineBreakAt + 1; currLineStart = lineBreakAt + 1;
this._currentInlineOffset = i - lineBreakAt - 1; this._currentInlineOffset = i - lineBreakAt - 1;
@ -137,7 +151,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
i++; i++;
} }
addLine(node.nodeValue.substring(currLineStart)); addLine(node.nodeValue.substring(currLineStart), highlight);
} }
return out; 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; var oldChildren = [], i;
for (i = 0; i < node.childNodes.length; i++) { for (i = 0; i < node.childNodes.length; i++) {
oldChildren.push(node.childNodes[i]); oldChildren.push(node.childNodes[i]);
@ -189,7 +203,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
for (i = 0; i < oldChildren.length; i++) { for (i = 0; i < oldChildren.length; i++) {
if (oldChildren[i].nodeType == TEXT_NODE) { 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++) { for (var j = 0; j < ret.length; j++) {
node.appendChild(ret[j]); node.appendChild(ret[j]);
} }
@ -201,8 +215,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
node.appendChild(this._createLineBreak()); node.appendChild(this._createLineBreak());
node.appendChild(this._createLineNumber()); node.appendChild(this._createLineNumber());
} }
var changedNode = this._insertLineNumbersToNode(oldChildren[i], length, highlight);
var changedNode = this._insertLineNumbersToNode(oldChildren[i], length);
this._moveLeadingLineBreaksToOuterNode(changedNode, node); this._moveLeadingLineBreaksToOuterNode(changedNode, node);
node.appendChild(changedNode); node.appendChild(changedNode);
} else { } else {
@ -253,7 +266,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
return Math.ceil(newLength); return Math.ceil(newLength);
}; };
this._insertLineNumbersToBlockNode = function (node, length) { this._insertLineNumbersToBlockNode = function (node, length, highlight) {
this._currentInlineOffset = 0; this._currentInlineOffset = 0;
this._prependLineNumberToFirstText = true; this._prependLineNumberToFirstText = true;
@ -268,7 +281,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
for (i = 0; i < oldChildren.length; i++) { for (i = 0; i < oldChildren.length; i++) {
if (oldChildren[i].nodeType == TEXT_NODE) { 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++) { for (var j = 0; j < ret.length; j++) {
node.appendChild(ret[j]); node.appendChild(ret[j]);
} }
@ -280,8 +293,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
node.appendChild(this._createLineBreak()); node.appendChild(this._createLineBreak());
node.appendChild(this._createLineNumber()); node.appendChild(this._createLineNumber());
} }
var changedNode = this._insertLineNumbersToNode(oldChildren[i], length, highlight);
var changedNode = this._insertLineNumbersToNode(oldChildren[i], length);
this._moveLeadingLineBreaksToOuterNode(changedNode, node); this._moveLeadingLineBreaksToOuterNode(changedNode, node);
node.appendChild(changedNode); node.appendChild(changedNode);
} else { } else {
@ -295,15 +307,15 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
return node; return node;
}; };
this._insertLineNumbersToNode = function (node, length) { this._insertLineNumbersToNode = function (node, length, highlight) {
if (node.nodeType !== ELEMENT_NODE) { if (node.nodeType !== ELEMENT_NODE) {
throw 'This method may only be called for ELEMENT-nodes: ' + node.nodeValue; throw 'This method may only be called for ELEMENT-nodes: ' + node.nodeValue;
} }
if (this._isInlineElement(node)) { if (this._isInlineElement(node)) {
return this._insertLineNumbersToInlineNode(node, length); return this._insertLineNumbersToInlineNode(node, length, highlight);
} else { } else {
var newLength = this._calcBlockNodeLength(node, length); 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; return root.innerHTML;
}; };
this.insertLineNumbersNode = function (html, lineLength) { this.insertLineNumbersNode = function (html, lineLength, highlight) {
var root = document.createElement('div'); var root = document.createElement('div');
root.innerHTML = html; root.innerHTML = html;
@ -337,11 +349,15 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
this._currentLineNumber = 1; this._currentLineNumber = 1;
this._prependLineNumberToFirstText = true; this._prependLineNumberToFirstText = true;
return this._insertLineNumbersToNode(root, lineLength); return this._insertLineNumbersToNode(root, lineLength, highlight);
}; };
this.insertLineNumbers = function (html, lineLength) { this.insertLineNumbers = function (html, lineLength, highlight, callback) {
var newRoot = this.insertLineNumbersNode(html, lineLength); var newRoot = this.insertLineNumbersNode(html, lineLength, highlight);
if (callback) {
callback();
}
return newRoot.innerHTML; return newRoot.innerHTML;
}; };

View File

@ -15,14 +15,69 @@ angular.module('OpenSlidesApp.motions.projector', ['OpenSlidesApp.motions'])
.controller('SlideMotionCtrl', [ .controller('SlideMotionCtrl', [
'$scope', '$scope',
'$rootScope',
'$http',
'Motion', 'Motion',
'User', 'User',
'Config', '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. // 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 // Add it to the coresponding get_requirements method of the ProjectorElement
// class. // class.
var id = $scope.element.id; 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'); Motion.bindOne(id, $scope, 'motion');
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');
} }

View File

@ -1069,9 +1069,10 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
'PdfMakeDocumentProvider', 'PdfMakeDocumentProvider',
'MotionInlineEditing', 'MotionInlineEditing',
'gettextCatalog', 'gettextCatalog',
'Projector',
function($scope, $http, ngDialog, MotionComment, MotionForm, Motion, Category, Mediafile, Tag, function($scope, $http, ngDialog, MotionComment, MotionForm, Motion, Category, Mediafile, Tag,
User, Workflow, Config, motion, SingleMotionContentProvider, MotionContentProvider, User, Workflow, Config, motion, SingleMotionContentProvider, MotionContentProvider,
PollContentProvider, PdfMakeConverter, PdfMakeDocumentProvider, MotionInlineEditing, gettextCatalog) { PollContentProvider, PdfMakeConverter, PdfMakeDocumentProvider, MotionInlineEditing, gettextCatalog, Projector) {
Motion.bindOne(motion.id, $scope, 'motion'); Motion.bindOne(motion.id, $scope, 'motion');
Category.bindAll({}, $scope, 'categories'); Category.bindAll({}, $scope, 'categories');
Mediafile.bindAll({}, $scope, 'mediafiles'); 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.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() { $scope.makePDF = function() {
var id = motion.identifier, var id = motion.identifier,
slice = Function.prototype.call.bind([].slice), slice = Function.prototype.call.bind([].slice),

View File

@ -287,7 +287,7 @@
</div> </div>
<div class="line-number-setter {{ lineNumberMode }}"> <div class="line-number-setter {{ lineNumberMode }}">
<div class="btn-group" data-toggle="buttons"> <span class="btn-group" data-toggle="buttons">
<div class="btn btn-default disabled" title="{{ 'Line Numbering' | translate }}"> <div class="btn btn-default disabled" title="{{ 'Line Numbering' | translate }}">
<i class="fa fa-list-ol" aria-hidden="true"></i> <i class="fa fa-list-ol" aria-hidden="true"></i>
</div> </div>
@ -309,9 +309,29 @@
ng-checked="lineNumberMode == 'outside'"> ng-checked="lineNumberMode == 'outside'">
<translate>Outside</translate> <translate>Outside</translate>
</label> </label>
</div> </span>
<span>
<form class="input-group" style="max-width: 220px;" ng-if="lineNumberMode != 'none'" ng-submit="scrollToAndHighlight(gotoLinenumber)">
<input type="number" class="form-control" ng-model="gotoLinenumber" placeholder="{{ 'Line' | translate }}"></input>
<span class="input-group-btn">
<button type="button" class="btn btn-default btn-slim" ng-show="gotoLinenumber"
ng-click="gotoLinenumber = ''; scrollToAndHighlight(0);">
<i class="fa fa-times text-danger"></i>
</button>
<button type="submit" class="btn btn-default">
<i class="fa fa-share"></i>
<translate>go</translate>
</button>
<button type="button" class="btn btn-default" os-perms="core.can_manage_projector"
ng-show="lineNumberMode != 'none' && motion.isProjected()" ng-click="toggleLinesForProjector()"
uib-tooltip="{{ 'Show highlighted line also on projector.' | translate }}">
<i class="fa" ng-class="linesForProjector ? 'fa-check-square-o' : 'fa-square-o'"></i>
<i class="fa fa-video-camera"></i>
</button>
</span>
</form>
</span>
</div> </div>
<div ng-class="{'col-sm-8': (lineNumberMode != 'outside'), 'col-sm-12': (lineNumberMode == 'outside')}"> <div ng-class="{'col-sm-8': (lineNumberMode != 'outside'), 'col-sm-12': (lineNumberMode == 'outside')}">
<div ng-if="motion.isAllowed('update') && version == motion.getVersion(-1).id"> <div ng-if="motion.isAllowed('update') && version == motion.getVersion(-1).id">
@ -319,7 +339,7 @@
<div ui-tinymce="inlineEditing.tinymceOptions" ng-model="inlineEditing.lineBrokenText" <div ui-tinymce="inlineEditing.tinymceOptions" ng-model="inlineEditing.lineBrokenText"
class="motion-text line-numbers-{{ lineNumberMode }}"></div> class="motion-text line-numbers-{{ lineNumberMode }}"></div>
</div> </div>
<div ng-show="!inlineEditing.active" ng-bind-html="motion.getTextWithLineBreaks(version) | trusted" <div ng-show="!inlineEditing.active" ng-bind-html="motion.getTextWithLineBreaks(version, highlight) | trusted"
class="motion-text line-numbers-{{ lineNumberMode }}"></div> class="motion-text line-numbers-{{ lineNumberMode }}"></div>
<div class="motion-save-toolbar" ng-class="{ 'visible': (inlineEditing.changed && inlineEditing.active) }"> <div class="motion-save-toolbar" ng-class="{ 'visible': (inlineEditing.changed && inlineEditing.active) }">
@ -332,7 +352,7 @@
</div> </div>
</div> </div>
<div ng-if="!(motion.isAllowed('update') && version == motion.getVersion(-1).id)"> <div ng-if="!(motion.isAllowed('update') && version == motion.getVersion(-1).id)">
<div ng-bind-html="motion.getTextWithLineBreaks(version) | trusted" <div ng-bind-html="motion.getTextWithLineBreaks(version, highlight) | trusted"
class="motion-text line-numbers-{{ lineNumberMode }}"></div> class="motion-text line-numbers-{{ lineNumberMode }}"></div>
</div> </div>

View File

@ -70,7 +70,7 @@
</div> </div>
<!-- Text --> <!-- Text -->
<div ng-bind-html="motion.getTextWithLineBreaks() | trusted" <div ng-bind-html="motion.getTextWithLineBreaks(null, line, scroll) | trusted"
class="motion-text line-numbers-{{ config('motions_default_line_numbering') }}"></div> class="motion-text line-numbers-{{ config('motions_default_line_numbering') }}"></div>
<!-- Reason --> <!-- Reason -->