Merge pull request #2360 from FinnStutzenstein/Issue2347
Highlighting and jumping to lines
This commit is contained in:
commit
53c4932171
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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>
|
||||
</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-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>
|
||||
|
||||
|
@ -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 -->
|
||||
|
Loading…
Reference in New Issue
Block a user