src/resources/js/scheduler.js
/**
* LUYA admin scheduler directive.
*
* The scheduler directive will turn any field into an interactive scheduling system.
*
* ```
* <luya-schedule
* value="{{currentValueOfTheEntity}}"
* primary-key-value="{{primaryKeyModelValue}}"
* model-class="luya\admin\models\User"
* attribute-name="is_deleted"
* title="Deleted Title"
* attribute-values="[{"label":"Draft","value":0},{"label":"Archived","value":2},{"label":"Published","value":1}]"
* />
* ```
*
* + value: Its a two-way binding name of field which contains the value
* + attribute-values: Its a two-way binding name of field which contains the current values to schedule and display based on an array with label and value key.
* + primary-key-value: Its a two-way binding mame of field which contains the current primary key. Composite keys must be seperated by commans like `1,3`.
* + model-class: The path to the model class which must impelement NgRestModelInterface
* + attribute-name: The name of the attribute which should be scheduled.
* + title: The title of the attribute, like the label.
*
* > Keep in mind to enable the queue fake cronjob or enable a cronjob which runs the queue command.
*
* @since 2.0
*/
zaa.directive("luyaSchedule", function() {
return {
restrict: 'E',
relace: true,
scope: {
value: "=",
attributeValues: "=",
primaryKeyValue: "=",
modelClass: "@",
attributeName: "@",
onlyIcon: "@",
title: "@"
},
controller: ['$scope', '$http', '$timeout', 'AdminToastService', function($scope, $http, $timeout, AdminToastService) {
// toggle window
$scope.getFirstAttributeKeyAsDefaultValue = function() {
return $scope.attributeValues[0]['value'];
};
$scope.newvalue = $scope.getFirstAttributeKeyAsDefaultValue();
$scope.isVisible = false;
$scope.upcomingAccordionOpen = true;
$scope.archiveAccordionOpen = false;
$scope.showDatepicker = false;
$scope.modalPositionClass = "";
$scope.$watch('showDatepicker', function(newValue) {
if (newValue === 0) {
// disable the schedule checkbox, ensure to reset the timestamp.
var date = new Date();
$scope.timestamp = date.getTime() / 1000;
}
});
$scope.toggleWindow = function() {
$scope.isVisible = !$scope.isVisible;
if ($scope.isVisible) {
$scope.getLogTable();
} else {
$scope.hideInlineModal();
}
};
$scope.escModal = function() {
if($scope.isVisible) {
$scope.isVisible = false;
$scope.hideInlineModal();
}
};
$scope.getUniqueFormId = function(prefix) {
return prefix + $scope.primaryKeyValue + '_' + $scope.attributeName;
};
// get existing job data
$scope.logs = {
'upcoming': [],
'archived': []
};
$scope.getLogTable = function(callback) {
$http.get('admin/api-admin-common/scheduler-log?model='+$scope.modelClass+'&pk=' + $scope.primaryKeyValue + '&target=' + $scope.attributeName).then(function(response) {
$scope.logs.archived = [];
$scope.logs.upcoming = [];
response.data.forEach(function(entry) {
if(entry.is_done) {
$scope.logs.archived.push(entry);
} else {
$scope.logs.upcoming.push(entry);
}
});
// check if latestId is done, if yes, maybe directly change the value for a given field.
angular.forEach($scope.logs.archived, function(value, key) {
if (value.id == $scope.latestId) {
$scope.value = value.new_attribute_value;
}
});
$timeout(function() {
$scope.showInlineModal();
});
});
};
$scope.valueToLabel = function(inputValue) {
var label;
angular.forEach($scope.attributeValues, function(value) {
if (value.value == inputValue) {
label = value.label;
}
});
return label;
};
// submit new job
var now = new Date().getTime() / 1000;
$scope.latestId;
$scope.timestamp = parseInt(now);
$scope.saveNewJob = function() {
$http.post('admin/api-admin-common/scheduler-add', {
model_class: $scope.modelClass,
primary_key: $scope.primaryKeyValue,
target_attribute_name: $scope.attributeName,
new_attribute_value: $scope.newvalue,
schedule_timestamp: $scope.timestamp
}).then(function(response) {
$scope.latestId = response.data.id;
$scope.getLogTable();
}, function(error) {
AdminToastService.errorArray(error.data);
});
};
$scope.deleteJob = function(job) {
$http.delete('admin/api-admin-common/scheduler-delete?id=' + job.id).then(function(response) {
$scope.getLogTable();
});
};
}],
link: function (scope, element, attr) {
var inlineModal = element.find('.inlinemodal');
var inlineModalArrow = element.find('.inlinemodal-arrow');
var button = element.find('.scheduler-btn');
// The spacing the modal has to the window border
// and button
var modalMargin = 15;
// If the space right to the button is smaller than minSpaceRight
// and the space to the left is bigger than to the right
// the modal will be aligned to the left of the button
var minSpaceRight = 500;
// If the space left or right of the button is smaller than minSpace
// the modal will be display in full width
var minSpace = 300;
// The max width of the modal, defined in the scss component inlinemodal
var maxWidth = 1000;
// Get the button position and align the modal to the right if
// it hast at least "minSpaceRight" spacing to the right
// If not, align left
scope.alignModal = function() {
var documentSize = {width: $(document).width(), height: element.parents('.luya-content')[0].scrollHeight || $(document).height()};
var buttonBcr = button[0].getBoundingClientRect();
var buttonSpaceRight = documentSize.width - (buttonBcr.left + buttonBcr.width + (modalMargin * 2));
var buttonSpaceLeft = buttonBcr.left - (modalMargin * 2);
var alignRight = buttonSpaceRight >= minSpaceRight || buttonSpaceRight >= buttonSpaceLeft;
var notEnoughSpace = buttonSpaceLeft < minSpace && buttonSpaceRight < minSpace;
inlineModal.removeClass('inlinemodal--left inlinemodal--right inlinemodal--full');
if(notEnoughSpace) {
inlineModal.addClass('inlinemodal--full');
inlineModal.css({
display: 'block',
left: modalMargin,
right: modalMargin,
top: modalMargin,
bottom: modalMargin
});
} else if (alignRight) {
inlineModal.addClass('inlinemodal--right');
inlineModal.css({
display: 'block',
left: buttonBcr.left + buttonBcr.width + modalMargin,
right: modalMargin,
width: 'auto'
});
} else {
inlineModal.addClass('inlinemodal--left');
inlineModal.css({
display: 'block',
left: buttonBcr.left > maxWidth ? 'auto' : modalMargin,
right: (documentSize.width + modalMargin) - buttonBcr.left,
width: buttonBcr.left > maxWidth ? '100%' : 'auto'
});
}
scope.alignModalArrow();
};
// Calculate the new top value for the arrow inside the inline modal
// We also check if the arrow is "inside" the modal and if not
// set a min or max top value
scope.alignModalArrow = function() {
var modalBcr = inlineModal[0].getBoundingClientRect();
var buttonBcr = button[0].getBoundingClientRect();
var arrowHeight = inlineModalArrow.outerHeight();
var newTop = buttonBcr.top - modalMargin - (arrowHeight / 2); // 7.5 equals the height of the arrow / 2
var topMin = 10;
var topMax = modalBcr.height - modalMargin - (arrowHeight / 2) - 10;
if (newTop <= topMin) {
newTop = topMin;
} else if (newTop >= (topMax > 0 ? topMax : 5000)) { // Top max might be below 0 if the modal bcr is 0
newTop = topMax;
}
inlineModalArrow.css({
top: newTop
});
};
scope.showInlineModal = function() {
scope.alignModal();
};
scope.hideInlineModal = function() {
inlineModal.css({display: 'none'});
};
var w = angular.element(window);
w.bind('resize', function() {
if(scope.isVisible) {
scope.alignModal();
}
});
$(window).on('scroll', function() {
if(scope.isVisible) {
scope.alignModalArrow();
}
});
element.parents().on('scroll', function() {
if(scope.isVisible) {
scope.alignModalArrow();
}
});
},
template: function () {
return '<div class="scheduler" ng-class="{\'inlinemodal--open\' : isVisible}">'+
'<button ng-click="toggleWindow()" type="button" class="scheduler-btn btn btn-link">' +
'<i class="material-icons">schedule</i><span ng-hide="onlyIcon">{{valueToLabel(value)}}</span>' +
'</button>' +
'<div class="inlinemodal" style="display: none;" ng-class="modalPositionClass" zaa-esc="escModal()">' +
'<div class="inlinemodal-inner">' +
'<div class="inlinemodal-head clearfix">' +
'<div class="modal-header">' +
'<h5 class="modal-title">{{title}}</h5>' +
'<div class="modal-close">' +
'<button type="button" class="close" aria-label="Close" ng-click="toggleWindow()">' +
'<span aria-hidden="true"><span class="modal-esc">ESC</span> ×</span>' +
'</button>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="inlinemodal-content">' +
'<div class="clearfix">' +
'<zaa-select model="newvalue" options="attributeValues" label="' + i18n['js_scheduler_new_value'] + '"></zaa-select>' +
'<zaa-checkbox model="showDatepicker" fieldid="{{getUniqueFormId(\'datepicker\')}}" label="' + i18n['js_scheduler_show_datepicker'] + '"></zaa-checkbox>'+
'<zaa-datetime ng-show="showDatepicker" model="timestamp" label="' + i18n['js_scheduler_time'] + '"></zaa-datetime>' +
'<button type="button" class="btn btn-save btn-icon float-right" ng-click="saveNewJob()">' + i18n['js_scheduler_save'] + '</button>' +
'</div>' +
'<div class="card mt-4" ng-class="{\'card-closed\': !upcomingAccordionOpen}" ng-hide="logs.upcoming.length <= 0">' +
'<div class="card-header" ng-click="upcomingAccordionOpen=!upcomingAccordionOpen">' +
'<span class="material-icons card-toggle-indicator">keyboard_arrow_down</span>' +
'<i class="material-icons">alarm</i> <span> ' + i18n['js_scheduler_title_upcoming'] + '</span><span class="badge badge-secondary float-right">{{logs.upcoming.length}}</span>' +
'</div>' +
'<div class="card-body p-2">' +
'<div class="table-responsive">' +
'<table class="table table-hover table-align-middle">' +
'<thead>' +
'<tr>' +
'<th>' + i18n['js_scheduler_table_newvalue'] + '</th>' +
'<th>' + i18n['js_scheduler_table_timestamp'] + '</th>' +
'<th></th>' +
'</tr>' +
'</thead>' +
'<tbody>' +
'<tr ng-repeat="log in logs.upcoming">' +
'<td>{{valueToLabel(log.new_attribute_value)}}</td>'+
'<td>{{log.schedule_timestamp*1000 | date:\'short\'}}</td>'+
'<td style="width: 60px;"><button type="button" class="btn btn-delete btn-icon" ng-click="deleteJob(log)"></button></td>'+
'</tr>' +
'</tbody>' +
'</table>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="card mt-3" ng-class="{\'card-closed\': !archiveAccordionOpen}" ng-hide="logs.archived.length <= 0">' +
'<div class="card-header" ng-click="archiveAccordionOpen=!archiveAccordionOpen">' +
'<span class="material-icons card-toggle-indicator">keyboard_arrow_down</span>' +
'<i class="material-icons">alarm_on</i> <span> ' + i18n['js_scheduler_title_completed'] + '</span><span class="badge badge-secondary float-right">{{logs.archived.length}}</span>' +
'</div>' +
'<div class="card-body p-2">' +
'<div class="table-responsive">' +
'<table class="table table-hover table-align-middle">' +
'<thead>' +
'<tr>' +
'<th>' + i18n['js_scheduler_table_newvalue'] + '</th>' +
'<th>' + i18n['js_scheduler_table_timestamp'] + '</th>' +
'</tr>' +
'</thead>' +
'<tbody>' +
'<tr ng-repeat="log in logs.archived">' +
'<td>{{valueToLabel(log.new_attribute_value)}}</td>'+
'<td>{{log.schedule_timestamp*1000 | date:\'short\'}}</td>' +
'</tr>' +
'</tbody>' +
'</table>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="inlinemodal-arrow"></div>' +
'</div>' +
'</div>';
}
};
});