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

View File

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

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
$scope.scroll = -5 * Projector.get(1).scroll;
$scope.scroll = -80 * Projector.get(1).scroll;
$scope.scale = 100 + 20 * Projector.get(1).scale;
}
});

View File

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

View File

@ -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/<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):
"""

View File

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

View File

@ -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 = '&nbsp;'; // 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;
};

View File

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

View File

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

View File

@ -287,7 +287,7 @@
</div>
<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 }}">
<i class="fa fa-list-ol" aria-hidden="true"></i>
</div>
@ -309,9 +309,29 @@
ng-checked="lineNumberMode == 'outside'">
<translate>Outside</translate>
</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 ng-class="{'col-sm-8': (lineNumberMode != 'outside'), 'col-sm-12': (lineNumberMode == 'outside')}">
<div ng-if="motion.isAllowed('update') && version == motion.getVersion(-1).id">
@ -319,7 +339,7 @@
<div ui-tinymce="inlineEditing.tinymceOptions" ng-model="inlineEditing.lineBrokenText"
class="motion-text line-numbers-{{ lineNumberMode }}"></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>
<div class="motion-save-toolbar" ng-class="{ 'visible': (inlineEditing.changed && inlineEditing.active) }">
@ -332,7 +352,7 @@
</div>
</div>
<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>
</div>

View File

@ -70,7 +70,7 @@
</div>
<!-- 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>
<!-- Reason -->