Refactor scroll handling into independent service

This commit is contained in:
gconsidine
2018-03-01 16:29:32 -05:00
committed by Jake McDermott
parent b16d9a89e3
commit 0c09447f2d
5 changed files with 578 additions and 461 deletions

View File

@@ -1,83 +1,41 @@
import Ansi from 'ansi-to-html';
import hasAnsi from 'has-ansi';
let vm;
let ansi;
let model;
let resource;
let page;
let container;
let $timeout;
let $sce;
let $compile;
let $scope;
let $q;
const record = {};
let parent = null;
const SCROLL_THRESHOLD = 0.1;
const SCROLL_DELAY = 1000;
const EVENT_START_TASK = 'playbook_on_task_start';
const EVENT_START_PLAY = 'playbook_on_play_start';
const EVENT_STATS_PLAY = 'playbook_on_stats';
const ELEMENT_TBODY = '#atStdoutResultTable';
const ELEMENT_CONTAINER = '.at-Stdout-container';
const JOB_START = 'playbook_on_start'; const JOB_START = 'playbook_on_start';
const JOB_END = 'playbook_on_stats'; const JOB_END = 'playbook_on_stats';
const EVENT_GROUPS = [ let vm;
EVENT_START_TASK, let $compile;
EVENT_START_PLAY let $scope;
]; let $q;
let page;
const TIME_EVENTS = [ let render;
EVENT_START_TASK, let scroll;
EVENT_START_PLAY, let resource;
EVENT_STATS_PLAY
];
function JobsIndexController ( function JobsIndexController (
_resource_, _resource_,
_page_, _page_,
_$sce_, _scroll_,
_$timeout_, _render_,
_$scope_, _$scope_,
_$compile_, _$compile_,
_$q_ _$q_
) { ) {
vm = this || {}; vm = this || {};
$timeout = _$timeout_;
$sce = _$sce_;
$compile = _$compile_; $compile = _$compile_;
$scope = _$scope_; $scope = _$scope_;
$q = _$q_; $q = _$q_;
resource = _resource_; resource = _resource_;
page = _page_; page = _page_;
model = resource.model; scroll = _scroll_;
render = _render_;
ansi = new Ansi();
const events = model.get(`related.${resource.related}.results`);
const parsed = parseEvents(events);
const html = $sce.trustAsHtml(parsed.html);
page.init(resource);
page.add({ number: 1, lines: parsed.lines });
// Development helper(s) // Development helper(s)
vm.clear = devClear; vm.clear = devClear;
// Stdout Navigation // Stdout Navigation
vm.scroll = { vm.scroll = {
isLocked: false,
showBackToTop: false, showBackToTop: false,
isActive: false,
position: 0,
time: 0,
home: scrollHome, home: scrollHome,
end: scrollEnd, end: scrollEnd,
down: scrollPageDown, down: scrollPageDown,
@@ -90,87 +48,93 @@ function JobsIndexController (
vm.isExpanded = true; vm.isExpanded = true;
// Real-time (active between JOB_START and JOB_END events only) // Real-time (active between JOB_START and JOB_END events only)
$scope.$on(resource.ws.namespace, processWebSocketEvents);
vm.stream = { vm.stream = {
isActive: false, active: false,
isRendering: false, rendering: false,
isPaused: false, paused: false
buffered: 0,
count: 0,
page: 1
}; };
window.requestAnimationFrame(() => { const stream = false; // TODO: Set in route
const table = $(ELEMENT_TBODY);
container = $(ELEMENT_CONTAINER);
table.html($sce.getTrustedHtml(html)); render.requestAnimationFrame(() => init());
$compile(table.contents())($scope);
container.scroll(onScroll);
});
} }
function processWebSocketEvents (scope, data) { function init (stream) {
let done; page.init(resource);
render.init({
get: () => resource.model.get(`related.${resource.related}.results`),
compile: html => $compile(html)($scope)
});
scroll.init({
isAtRest: scrollIsAtRest,
previous,
next
});
if (stream) {
$scope.$on(resource.ws.namespace, process);
} else {
next();
}
}
function process (scope, data) {
if (data.event === JOB_START) { if (data.event === JOB_START) {
vm.scroll.isActive = true; vm.stream.active = true;
vm.stream.isActive = true; scroll.lock();
vm.scroll.isLocked = true;
} else if (data.event === JOB_END) { } else if (data.event === JOB_END) {
vm.stream.isActive = false; vm.stream.active = false;
} }
const pageAdded = page.addToBuffer(data); const pageAdded = page.addToBuffer(data);
if (pageAdded && !vm.scroll.isLocked) { if (pageAdded && !scroll.isLocked()) {
vm.stream.isPaused = true; vm.stream.paused = true;
} }
if (vm.stream.isPaused && vm.scroll.isLocked) { if (vm.stream.paused && scroll.isLocked()) {
vm.stream.isPaused = false; vm.stream.paused = false;
} }
if (vm.stream.isRendering || vm.stream.isPaused) { if (vm.stream.rendering || vm.stream.paused) {
return; return;
} }
const events = page.emptyBuffer(); const events = page.emptyBuffer();
return render(events); return renderStream(events);
} }
function render (events) { function renderStream (events) {
vm.stream.isRendering = true; vm.stream.rendering = true;
return shift() return shift()
.then(() => append(events)) .then(() => append(events))
.then(() => { .then(() => {
if (vm.scroll.isLocked) { if (scroll.isLocked()) {
const height = container[0].scrollHeight; scroll.setScrollPosition(scroll.getScrollHeight());
container[0].scrollTop = height;
} }
if (!vm.stream.isActive) { if (!vm.stream.active) {
const buffer = page.emptyBuffer(); const buffer = page.emptyBuffer();
if (buffer.length) { if (buffer.length) {
return render(buffer); return renderStream(buffer);
} else {
vm.stream.rendering = false;
scroll.unlock();
} }
vm.stream.isRendering = false;
vm.scroll.isLocked = false;
vm.scroll.isActive = false;
} else { } else {
vm.stream.isRendering = false; vm.stream.rendering = false;
} }
}); });
} }
function devClear () { function devClear () {
page.init(resource); init(true);
clear(); render.clear();
} }
function next () { function next () {
@@ -182,13 +146,12 @@ function next () {
return shift() return shift()
.then(() => append(events)); .then(() => append(events));
}) });
} }
function previous () { function previous () {
const container = $(ELEMENT_CONTAINER)[0]; let initialPosition = scroll.getScrollPosition();
let postPopHeight;
let previousHeight;
return page.previous() return page.previous()
.then(events => { .then(events => {
@@ -198,296 +161,56 @@ function previous () {
return pop() return pop()
.then(() => { .then(() => {
previousHeight = container.scrollHeight; postPopHeight = scroll.getScrollHeight();
return prepend(events); return prepend(events);
}) })
.then(() => { .then(() => {
const currentHeight = container.scrollHeight; const currentHeight = scroll.getScrollHeight();
container.scrollTop = currentHeight - previousHeight;
scroll.setScrollPosition(currentHeight - postPopHeight + initialPosition);
}); });
}); });
} }
function append (events) { function append (events) {
return $q(resolve => { return render.append(events)
window.requestAnimationFrame(() => { .then(count => {
const parsed = parseEvents(events); page.updateLineCount('current', count);
const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parsed.html)));
const table = $(ELEMENT_TBODY);
page.updateLineCount('current', parsed.lines);
table.append(rows);
$compile(rows.contents())($scope);
return resolve();
}); });
});
} }
function prepend (events) { function prepend (events) {
return $q(resolve => { return render.prepend(events)
window.requestAnimationFrame(() => { .then(count => {
const parsed = parseEvents(events); page.updateLineCount('current', count);
const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parsed.html)));
const table = $(ELEMENT_TBODY);
page.updateLineCount('current', parsed.lines);
table.prepend(rows);
$compile(rows.contents())($scope);
$scope.$apply(() => {
return resolve(parsed.lines);
});
}); });
});
} }
function pop () { function pop () {
return $q(resolve => { if (!page.isOverCapacity()) {
if (!page.isOverCapacity()) { return $q.resolve();
return resolve(); }
}
window.requestAnimationFrame(() => { const lines = page.trim('right');
const lines = page.trim('right');
const rows = $(ELEMENT_TBODY).children().slice(-lines);
rows.empty(); return render.pop(lines);
rows.remove();
return resolve();
});
});
} }
function shift () { function shift () {
return $q(resolve => { if (!page.isOverCapacity()) {
if (!page.isOverCapacity()) { return $q.resolve();
return resolve(); }
}
window.requestAnimationFrame(() => { const lines = page.trim('left');
const lines = page.trim('left');
const rows = $(ELEMENT_TBODY).children().slice(0, lines);
rows.empty(); return render.shift(lines);
rows.remove();
return resolve();
});
});
}
function clear () {
return $q(resolve => {
window.requestAnimationFrame(() => {
const rows = $(ELEMENT_TBODY).children();
rows.empty();
rows.remove();
return resolve();
});
});
} }
function expand () { function expand () {
vm.toggle(parent, true); vm.toggle(parent, true);
} }
function parseEvents (events) {
let lines = 0;
let html = '';
events.sort(orderByLineNumber);
events.forEach(event => {
const line = parseLine(event);
html += line.html;
lines += line.count;
});
return {
html,
lines
};
}
function orderByLineNumber (a, b) {
if (a.start_line > b.start_line) {
return 1;
}
if (a.start_line < b.start_line) {
return -1;
}
return 0;
}
function parseLine (event) {
if (!event || !event.stdout) {
return { html: '', count: 0 };
}
const { stdout } = event;
const lines = stdout.split('\r\n');
let count = lines.length;
let ln = event.start_line;
const current = createRecord(ln, lines, event);
const html = lines.reduce((html, line, i) => {
ln++;
const isLastLine = i === lines.length - 1;
let row = createRow(current, ln, line);
if (current && current.isTruncated && isLastLine) {
row += createRow(current);
count++;
}
return `${html}${row}`;
}, '');
return { html, count };
}
function createRecord (ln, lines, event) {
if (!event.uuid) {
return null;
}
const info = {
id: event.id,
line: ln + 1,
uuid: event.uuid,
level: event.event_level,
start: event.start_line,
end: event.end_line,
isTruncated: (event.end_line - event.start_line) > lines.length,
isHost: typeof event.host === 'number'
};
if (event.parent_uuid) {
info.parents = getParentEvents(event.parent_uuid);
}
if (info.isTruncated) {
info.truncatedAt = event.start_line + lines.length;
}
if (EVENT_GROUPS.includes(event.event)) {
info.isParent = true;
if (event.event_level === 1) {
parent = event.uuid;
}
if (event.parent_uuid) {
if (record[event.parent_uuid]) {
if (record[event.parent_uuid].children &&
!record[event.parent_uuid].children.includes(event.uuid)) {
record[event.parent_uuid].children.push(event.uuid);
} else {
record[event.parent_uuid].children = [event.uuid];
}
}
}
}
if (TIME_EVENTS.includes(event.event)) {
info.time = getTime(event.created);
info.line++;
}
record[event.uuid] = info;
return info;
}
function getParentEvents (uuid, list) {
list = list || [];
if (record[uuid]) {
list.push(uuid);
if (record[uuid].parents) {
list = list.concat(record[uuid].parents);
}
}
return list;
}
function createRow (current, ln, content) {
let id = '';
let timestamp = '';
let tdToggle = '';
let tdEvent = '';
let classList = '';
content = content || '';
if (hasAnsi(content)) {
content = ansi.toHtml(content);
}
if (current) {
if (current.isParent && current.line === ln) {
id = current.uuid;
tdToggle = `<td class="at-Stdout-toggle" ng-click="vm.toggle('${id}')"><i class="fa fa-angle-down can-toggle"></i></td>`;
}
if (current.isHost) {
tdEvent = `<td class="at-Stdout-event--host" ng-click="vm.showHostDetails('${current.id}')">${content}</td>`;
}
if (current.time && current.line === ln) {
timestamp = `<span>${current.time}</span>`;
}
if (current.parents) {
classList = current.parents.reduce((list, uuid) => `${list} child-of-${uuid}`, '');
}
}
if (!tdEvent) {
tdEvent = `<td class="at-Stdout-event">${content}</td>`;
}
if (!tdToggle) {
tdToggle = '<td class="at-Stdout-toggle"></td>';
}
if (!ln) {
ln = '...';
}
return `
<tr id="${id}" class="${classList}">
${tdToggle}
<td class="at-Stdout-line">${ln}</td>
${tdEvent}
<td class="at-Stdout-time">${timestamp}</td>
</tr>`;
}
function getTime (created) {
const date = new Date(created);
const hour = date.getHours() < 10 ? `0${date.getHours()}` : date.getHours();
const minute = date.getMinutes() < 10 ? `0${date.getMinutes()}` : date.getMinutes();
const second = date.getSeconds() < 10 ? `0${date.getSeconds()}` : date.getSeconds();
return `${hour}:${minute}:${second}`;
}
function showHostDetails (id) { function showHostDetails (id) {
jobEvent.request('get', id) jobEvent.request('get', id)
.then(() => { .then(() => {
@@ -527,100 +250,38 @@ function toggle (uuid, menu) {
} }
} }
function onScroll () {
if (vm.scroll.isActive) {
return;
}
if (vm.scroll.register) {
$timeout.cancel(vm.scroll.register);
}
vm.scroll.register = $timeout(registerScrollEvent, SCROLL_DELAY);
}
function registerScrollEvent () {
vm.scroll.isActive = true;
const position = container[0].scrollTop;
const height = container[0].offsetHeight;
const downward = position > vm.scroll.position;
let promise;
if (position !== 0 ) {
vm.scroll.showBackToTop = true;
} else {
vm.scroll.showBackToTop = false;
}
console.log('downward', downward);
if (downward) {
if (((height - position) / height) < SCROLL_THRESHOLD) {
promise = next;
}
} else {
if ((position / height) < SCROLL_THRESHOLD) {
console.log('previous');
promise = previous;
}
}
vm.scroll.position = position;
if (!promise) {
vm.scroll.isActive = false;
return $q.resolve();
}
return promise()
.then(() => {
console.log('done');
vm.scroll.isActive = false;
/*
*$timeout(() => {
* vm.scroll.isActive = false;
*}, SCROLL_DELAY);
*/
});
}
function scrollHome () { function scrollHome () {
scroll.pause();
return page.first() return page.first()
.then(events => { .then(events => {
if (!events) { if (!events) {
return; return;
} }
return clear() return render.clear()
.then(() => prepend(events)) .then(() => render.prepend(events))
.then(() => { .then(() => {
vm.scroll.isActive = false; scroll.setScrollPosition(0);
scroll.resume();
}); });
}); });
} }
function scrollEnd () { function scrollEnd () {
if (vm.scroll.isLocked) { if (scroll.isLocked()) {
page.bookmark(); page.bookmark();
scroll.unlock();
vm.scroll.isLocked = false;
vm.scroll.isActive = false;
return; return;
} else if (!vm.scroll.isLocked && vm.stream.isActive) { } else if (!scroll.isLocked() && vm.stream.active) {
page.bookmark(); page.bookmark();
scroll.lock();
vm.scroll.isActive = true;
vm.scroll.isLocked = true;
return; return;
} }
vm.scroll.isActive = true; scroll.pause();
return page.last() return page.last()
.then(events => { .then(events => {
@@ -628,36 +289,32 @@ function scrollEnd () {
return; return;
} }
return clear() return render.clear()
.then(() => append(events)) .then(() => render.append(events))
.then(() => { .then(() => {
const container = $(ELEMENT_CONTAINER)[0]; scroll.setScrollPosition(scroll.getScrollHeight());
scroll.resume();
container.scrollTop = container.scrollHeight;
vm.scroll.isActive = false;
}); });
}); });
} }
function scrollPageUp () { function scrollPageUp () {
const container = $(ELEMENT_CONTAINER)[0]; scroll.pageUp();
const jump = container.scrollTop - container.offsetHeight;
container.scrollTop = jump;
} }
function scrollPageDown () { function scrollPageDown () {
const container = $(ELEMENT_CONTAINER)[0]; scroll.pageDown();
const jump = container.scrollTop + container.offsetHeight; }
container.scrollTop = jump; function scrollIsAtRest (isAtRest) {
vm.scroll.showBackToTop = !isAtRest;
} }
JobsIndexController.$inject = [ JobsIndexController.$inject = [
'resource', 'resource',
'JobPageService', 'JobPageService',
'$sce', 'JobScrollService',
'$timeout', 'JobRenderService',
'$scope', '$scope',
'$compile', '$compile',
'$q' '$q'

View File

@@ -1,11 +1,10 @@
import JobsStrings from '~features/output/jobs.strings';
import IndexController from '~features/output/index.controller';
import atLibModels from '~models'; import atLibModels from '~models';
import atLibComponents from '~components'; import atLibComponents from '~components';
import Strings from '~features/output/jobs.strings'; import Strings from '~features/output/jobs.strings';
import Controller from '~features/output/index.controller'; import Controller from '~features/output/index.controller';
import PageService from '~features/output/page.service'; import PageService from '~features/output/page.service';
import ScrollService from '~features/output/scroll.service';
const Template = require('~features/output/index.view.html'); const Template = require('~features/output/index.view.html');
@@ -178,6 +177,7 @@ angular
]) ])
.service('JobStrings', Strings) .service('JobStrings', Strings)
.service('JobPageService', PageService) .service('JobPageService', PageService)
.service('JobScrollService', ScrollService)
.run(JobsRun); .run(JobsRun);
export default MODULE_NAME; export default MODULE_NAME;

View File

@@ -1,16 +1,10 @@
function JobPageService ($q) { function JobPageService ($q) {
this.page = null;
this.resource = null;
this.result = null;
this.buffer = null;
this.cache = null;
this.init = resource => { this.init = resource => {
this.resource = resource; this.resource = resource;
this.page = { this.page = {
limit: resource.page.pageLimit, limit: this.resource.page.pageLimit,
size: resource.page.size, size: this.resource.page.size,
current: 0, current: 0,
index: -1, index: -1,
count: 0, count: 0,
@@ -153,13 +147,10 @@ function JobPageService ($q) {
} }
this.bookmark = () => { this.bookmark = () => {
console.log('b,current', this.page.current);
if (!this.page.bookmark.active) { if (!this.page.bookmark.active) {
this.page.bookmark.first = this.page.first; this.page.bookmark.first = this.page.first;
this.page.bookmark.last = this.page.last; this.page.bookmark.last = this.page.last;
this.page.bookmark.current = this.page.current; this.page.bookmark.current = this.page.current;
console.log('b,bookmark', this.page.bookmark.current);
this.page.bookmark.active = true; this.page.bookmark.active = true;
} else { } else {
this.page.bookmark.active = false; this.page.bookmark.active = false;

View File

@@ -0,0 +1,297 @@
import Ansi from 'ansi-to-html';
import hasAnsi from 'has-ansi';
const ELEMENT_TBODY = '#atStdoutResultTable';
const EVENT_START_TASK = 'playbook_on_task_start';
const EVENT_START_PLAY = 'playbook_on_play_start';
const EVENT_STATS_PLAY = 'playbook_on_stats';
const JOB_START = 'playbook_on_start';
const JOB_END = 'playbook_on_stats';
const EVENT_GROUPS = [
EVENT_START_TASK,
EVENT_START_PLAY
];
const TIME_EVENTS = [
EVENT_START_TASK,
EVENT_START_PLAY,
EVENT_STATS_PLAY
];
const ansi = new Ansi();
function JobRenderService ($q, $sce, $window) {
this.init = ({ compile, apply, get }) => {
this.parent = null;
this.record = {};
this.el = $(ELEMENT_TBODY);
this.hooks = { get, compile, apply };
};
this.sortByLineNumber = (a, b) => {
if (a.start_line > b.start_line) {
return 1;
}
if (a.start_line < b.start_line) {
return -1;
}
return 0;
};
this.transformEventGroup = events => {
let lines = 0;
let html = '';
events.sort(this.sortByLineNumber);
events.forEach(event => {
const line = this.transformEvent(event);
html += line.html;
lines += line.count;
});
return { html, lines };
};
this.transformEvent = event => {
if (!event || !event.stdout) {
return { html: '', count: 0 };
}
const { stdout } = event;
const lines = stdout.split('\r\n');
let count = lines.length;
let ln = event.start_line;
const current = this.createRecord(ln, lines, event);
const html = lines.reduce((html, line, i) => {
ln++;
const isLastLine = i === lines.length - 1;
let row = this.createRow(current, ln, line);
if (current && current.isTruncated && isLastLine) {
row += this.createRow(current);
count++;
}
return `${html}${row}`;
}, '');
return { html, count };
};
this.createRecord = event => {
if (!event.uuid) {
return null;
}
const info = {
id: event.id,
line: ln + 1,
uuid: event.uuid,
level: event.event_level,
start: event.start_line,
end: event.end_line,
isTruncated: (event.end_line - event.start_line) > lines.length,
isHost: typeof event.host === 'number'
};
if (event.parent_uuid) {
info.parents = getParentEvents(event.parent_uuid);
}
if (info.isTruncated) {
info.truncatedAt = event.start_line + lines.length;
}
if (EVENT_GROUPS.includes(event.event)) {
info.isParent = true;
if (event.event_level === 1) {
this.parent = event.uuid;
}
if (event.parent_uuid) {
if (this.record[event.parent_uuid]) {
if (this.record[event.parent_uuid].children &&
!this.record[event.parent_uuid].children.includes(event.uuid)) {
this.record[event.parent_uuid].children.push(event.uuid);
} else {
this.record[event.parent_uuid].children = [event.uuid];
}
}
}
}
if (TIME_EVENTS.includes(event.event)) {
info.time = this.getTimestamp(event.created);
info.line++;
}
this.record[event.uuid] = info;
return info;
};
this.createRow = (current, ln, content) => {
let id = '';
let timestamp = '';
let tdToggle = '';
let tdEvent = '';
let classList = '';
content = content || '';
if (hasAnsi(content)) {
content = ansi.toHtml(content);
}
if (current) {
if (current.isParent && current.line === ln) {
id = current.uuid;
tdToggle = `<td class="at-Stdout-toggle" ng-click="vm.toggle('${id}')"><i class="fa fa-angle-down can-toggle"></i></td>`;
}
if (current.isHost) {
tdEvent = `<td class="at-Stdout-event--host" ng-click="vm.showHostDetails('${current.id}')">${content}</td>`;
}
if (current.time && current.line === ln) {
timestamp = `<span>${current.time}</span>`;
}
if (current.parents) {
classList = current.parents.reduce((list, uuid) => `${list} child-of-${uuid}`, '');
}
}
if (!tdEvent) {
tdEvent = `<td class="at-Stdout-event">${content}</td>`;
}
if (!tdToggle) {
tdToggle = '<td class="at-Stdout-toggle"></td>';
}
if (!ln) {
ln = '...';
}
return `
<tr id="${id}" class="${classList}">
${tdToggle}
<td class="at-Stdout-line">${ln}</td>
${tdEvent}
<td class="at-Stdout-time">${timestamp}</td>
</tr>`;
}
this.getTimestamp = (created) => {
const date = new Date(created);
const hour = date.getHours() < 10 ? `0${date.getHours()}` : date.getHours();
const minute = date.getMinutes() < 10 ? `0${date.getMinutes()}` : date.getMinutes();
const second = date.getSeconds() < 10 ? `0${date.getSeconds()}` : date.getSeconds();
return `${hour}:${minute}:${second}`;
}
this.getParentEvents = (uuid, list) => {
list = list || [];
if (this.record[uuid]) {
list.push(uuid);
if (this.record[uuid].parents) {
list = list.concat(record[uuid].parents);
}
}
return list;
};
this.getEvents = () => {
return this.hooks.get();
};
this.insert = (events, insert) => {
const result = this.transformEventGroup(events);
const html = this.sanitize(result.html);
return this.requestAnimationFrame(() => insert(html))
.then(() => this.compile(html))
.then(() => result.lines);
};
this.remove = elements => {
return this.requestAnimationFrame(() => {
elements.empty();
elements.remove();
});
};
this.requestAnimationFrame = fn => {
return $q(resolve => {
$window.requestAnimationFrame(() => {
if (fn) {
fn();
}
return resolve();
});
});
};
this.compile = html => {
this.hooks.compile(html);
return this.requestAnimationFrame();
};
this.build = () => {
};
this.clear = () => {
const elements = this.el.children();
return this.remove(elements);
};
this.shift = lines => {
const elements = this.el.children().slice(0, lines);
return this.remove(elements);
};
this.pop = lines => {
const elements = this.el.children().slice(-lines);
return this.remove(elements);
};
this.prepend = events => {
return this.insert(events, html => this.el.prepend(html))
};
this.append = events => {
return this.insert(events, html => this.el.append(html))
};
// TODO: stdout from the API should not be trusted.
this.sanitize = html => {
html = $sce.trustAsHtml(html);
return $sce.getTrustedHtml(html);
};
}
JobRenderService.$inject = ['$q', '$sce', '$window'];
export default JobRenderService;

View File

@@ -0,0 +1,172 @@
const ELEMENT_CONTAINER = '.at-Stdout-container';
const DELAY = 100;
const THRESHOLD = 0.1;
function JobScrollService ($q, $timeout) {
this.init = (hooks) => {
this.el = $(ELEMENT_CONTAINER);
this.timer = null;
this.position = {
previous: 0,
current: 0
};
this.hooks = {
isAtRest: hooks.isAtRest,
next: hooks.next,
previous: hooks.previous
};
this.state = {
locked: false,
paused: false,
top: true
};
this.el.scroll(this.listen);
};
this.listen = () => {
if (this.isPaused()) {
return;
}
if (this.timer) {
$timeout.cancel(this.timer);
}
this.timer = $timeout(this.register, DELAY);
};
this.register = () => {
this.pause();
const height = this.getScrollHeight();
const current = this.getScrollPosition();
const downward = current > this.position.previous;
let promise;
if (downward && this.isBeyondThreshold(downward, current)) {
promise = this.hooks.next;
} else if (!downward && this.isBeyondThreshold(downward, current)) {
promise = this.hooks.previous;
}
if (!promise) {
this.setScrollPosition(current);
this.isAtRest();
this.resume();
return $q.resolve();
}
return promise()
.then(() => {
this.setScrollPosition(this.getScrollPosition());
this.isAtRest();
this.resume();
});
};
this.isBeyondThreshold = (downward, current) => {
const previous = this.position.previous;
const height = this.getScrollHeight();
if (downward) {
current += this.getViewableHeight();
if (current >= height || ((height - current) / height) < THRESHOLD) {
return true;
}
} else {
if (current <= 0 || (current / height) < THRESHOLD) {
return true;
}
}
return false;
};
this.pageUp = () => {
if (this.isPaused()) {
return;
}
const top = this.getScrollPosition();
const height = this.getViewableHeight();
this.setScrollPosition(top - height);
};
this.pageDown = () => {
if (this.isPaused()) {
return;
}
const top = this.getScrollPosition();
const height = this.getViewableHeight();
this.setScrollPosition(top + height);
};
this.getScrollHeight = () => {
return this.el[0].scrollHeight;
};
this.getViewableHeight = () => {
return this.el[0].offsetHeight;
};
this.getScrollPosition = () => {
return this.el[0].scrollTop;
};
this.setScrollPosition = position => {
this.position.previous = this.position.current;
this.position.current = position;
this.el[0].scrollTop = position;
this.isAtRest();
};
this.isAtRest = () => {
if (this.position.current === 0 && !this.state.top) {
this.state.top = true;
this.hooks.isAtRest(true);
} else if (this.position.current > 0 && this.state.top) {
this.state.top = false;
this.hooks.isAtRest(false);
}
};
this.resume = () => {
this.state.paused = false;
};
this.pause = () => {
this.state.paused = true;
};
this.isPaused = () => {
return this.state.paused;
};
this.lock = () => {
this.state.locked = true;
this.state.paused = true;
};
this.unlock = () => {
this.state.locked = false;
this.state.paused = false;
};
this.isLocked = () => {
return this.state.locked;
};
}
JobScrollService.$inject = ['$q', '$timeout'];
export default JobScrollService;