Display more than 100 markers in angularjs google maps with rotation - angularjs

I have been using ngMap with my angularjs code for displaying markers. However, with more than 100 markers I have noticed that there is a considerable decrease in performance mainly related to ng-repeat and two way binding. I would like to add markers with custom HTML elements similar to CustomMarker but using ordinary Markers and modified from controller when required.
Challenges faced :
I have SVG images which need to be dynamically coloured based on the conditions (These SVGs are not single path ones, hence doesn't seem to work well when I used it with Symbol)
These are vehicle markers and hence should support rotation

I have solved this by creating CustomMarker with Overlay and then adding the markers that are only present in the current map bounds to the map so that map doesn't lag.
Below is the code snippet with which I achieved it.
createCustomMarkerComponent();
/**
* options : [Object] : options to be passed on
* - position : [Object] : Google maps latLng object
* - map : [Object] : Google maps instance
* - markerId : [String] : Marker id
* - innerHTML : [String] : innerHTML string for the marker
**/
function CustomMarker(options) {
var self = this;
self.options = options || {};
self.el = document.createElement('div');
self.el.style.display = 'block';
self.el.style.visibility = 'hidden';
self.visible = true;
self.display = false;
for (var key in options) {
self[key] = options[key];
}
self.setContent();
google.maps.event.addListener(self.options.map, "idle", function (event) {
//This is the current user-viewable region of the map
var bounds = self.options.map.getBounds();
checkElementVisibility(self, bounds);
});
if (this.options.onClick) {
google.maps.event.addDomListener(this.el, "click", this.options.onClick);
}
}
function checkElementVisibility(item, bounds) {
//checks if marker is within viewport and displays the marker accordingly - triggered by google.maps.event "idle" on the map Object
if (bounds.contains(item.position)) {
//If the item isn't already being displayed
if (item.display != true) {
item.display = true;
item.setMap(item.options.map);
}
} else {
item.display = false;
item.setMap(null);
}
}
var supportedTransform = (function getSupportedTransform() {
var prefixes = 'transform WebkitTransform MozTransform OTransform msTransform'.split(' ');
var div = document.createElement('div');
for (var i = 0; i < prefixes.length; i++) {
if (div && div.style[prefixes[i]] !== undefined) {
return prefixes[i];
}
}
return false;
})();
function createCustomMarkerComponent() {
if (window.google) {
CustomMarker.prototype = new google.maps.OverlayView();
CustomMarker.prototype.setContent = function () {
this.el.innerHTML = this.innerHTML;
this.el.style.position = 'absolute';
this.el.style.cursor = 'pointer';
this.el.style.top = 0;
this.el.style.left = 0;
};
CustomMarker.prototype.getPosition = function () {
return this.position;
};
CustomMarker.prototype.getDraggable = function () {
return this.draggable;
};
CustomMarker.prototype.setDraggable = function (draggable) {
this.draggable = draggable;
};
CustomMarker.prototype.setPosition = function (position) {
var self = this;
return new Promise(function () {
position && (self.position = position); /* jshint ignore:line */
if (self.getProjection() && typeof self.position.lng == 'function') {
var setPosition = function () {
if (!self.getProjection()) {
return;
}
var posPixel = self.getProjection().fromLatLngToDivPixel(self.position);
var x = Math.round(posPixel.x - (self.el.offsetWidth / 2));
var y = Math.round(posPixel.y - self.el.offsetHeight + 10); // 10px for anchor; 18px for label if not position-absolute
if (supportedTransform) {
self.el.style[supportedTransform] = "translate(" + x + "px, " + y + "px)";
} else {
self.el.style.left = x + "px";
self.el.style.top = y + "px";
}
self.el.style.visibility = "visible";
};
if (self.el.offsetWidth && self.el.offsetHeight) {
setPosition();
} else {
//delayed left/top calculation when width/height are not set instantly
setTimeout(setPosition, 300);
}
}
});
};
CustomMarker.prototype.setZIndex = function (zIndex) {
if (zIndex === undefined) return;
(this.zIndex !== zIndex) && (this.zIndex = zIndex); /* jshint ignore:line */
(this.el.style.zIndex !== this.zIndex) && (this.el.style.zIndex = this.zIndex);
};
CustomMarker.prototype.getVisible = function () {
return this.visible;
};
CustomMarker.prototype.setVisible = function (visible) {
if (this.el.style.display === 'none' && visible) {
this.el.style.display = 'block';
} else if (this.el.style.display !== 'none' && !visible) {
this.el.style.display = 'none';
}
this.visible = visible;
};
CustomMarker.prototype.addClass = function (className) {
var classNames = this.el.className.trim().split(' ');
(classNames.indexOf(className) == -1) && classNames.push(className); /* jshint ignore:line */
this.el.className = classNames.join(' ');
};
CustomMarker.prototype.removeClass = function (className) {
var classNames = this.el.className.split(' ');
var index = classNames.indexOf(className);
(index > -1) && classNames.splice(index, 1); /* jshint ignore:line */
this.el.className = classNames.join(' ');
};
CustomMarker.prototype.onAdd = function () {
this.getPanes().overlayMouseTarget.appendChild(this.el);
// this.getPanes().markerLayer.appendChild(label-div); // ??
};
CustomMarker.prototype.draw = function () {
this.setPosition();
this.setZIndex(this.zIndex);
this.setVisible(this.visible);
};
CustomMarker.prototype.onRemove = function () {
this.el.parentNode.removeChild(this.el);
// this.el = null;
};
} else {
setTimeout(createCustomMarkerComponent, 200);
}
}
The checkElementVisibility function helps in identifying whether a marker should appear or not.
In case there are better solutions please add it here.Thanks!

Related

How can I link data coming from an API to a column in a data table in Angularjs

There is a data table which is getting data from an API.
This API is responsible for populating every column on the data table. I added a new column named "Project Involved" which should get filled by the data "ProjectNames" coming from API.
The problem that am facing is- I am not able to link this data to the column on data table.
There are other columns that are getting populated.
This is the column name:
{
data: 'ProjectsInvolved',
title: $translate.instant('Projects involved'),
width: 80
}
This is where we link the API to the data table:
"use strict";
angular
.module('project')
.controller(
'ProjectPermissionsGridController',
function (
$scope,
roleConstants,
$timeout,
FormHelperService,
$translate,
$mdDialog,
logger,
projectConstants,
DateHelperService,
ResourceService,
ProjectService,
$stateParams,
$filter
) {
var pm = this;
pm.resources = [];
pm.copyOfAllResources = [];
pm.pmFormBtn = $translate.instant('btn.save');
pm.projectMemberList = [];
pm.IsProjectManager = false;
pm.maxAllowedHoursLimit = projectConstants.PROJECT_COST_PER_HOUR;
pm.projectMembersData = {
EmpID: '',
Read: roleConstants.NONE,
Write: roleConstants.NONE,
IsProjectManager: false,
// ProjectNames: pm.projectMemberList.ProjectNames
ProjectNames: ''
};
pm.showNoErrorsPerm = true;
pm.maxDate = DateHelperService.setTimeFormattedObject(new Date());
var currentIndex = '';
pm.isProjectMemberEdit = false;
var oldProjectMemberData = {};
pm.udpateResourcePersmissions = function () {
if (pm.projectMembersData.IsProjectManager) {
pm.projectMembersData.Read = roleConstants.ALL;
pm.projectMembersData.Write = roleConstants.ALL;
}
};
pm.initDataTableConfig = function (column, config, api) {
$scope.dtColumns = column;
$scope.dtConfig = config;
$scope.dtApiProjectMembers = api;
};
// Catching the event and binding the data
$scope.$on('evt.project-member-details', function (e, projectMembers) {
pm.projectMemberList = projectMembers;
pm.bindDataToGrid(false, projectMembers);
});
$scope.$on('evt.resources-list', function (e, resources) {
pm.resources = resources;
pm.copyOfAllResources = resources;
});
// On change to remove the cost per hour required validation.
pm.removeCostperHourValidation = function () {
if (!pm.projectMembersData.CostPerHour) {
pm.projectMembersData.CostPerHour = null;
}
if (
parseFloat(pm.projectMembersData.CostPerHour) >
pm.maxAllowedHoursLimit
) {
pm.projectMembersData.CostPerHour = pm.maxAllowedHoursLimit;
}
};
/**
* Add / update new data to project members grid.
*/
pm.addNewProjectMember = function (valid, $event) {
if (!valid) {
$scope.showNoErrorsPerm = false;
return false;
}
pm.projectMembersData.ProjectMember =
FormHelperService.getSelectedObject(
pm.resources,
'EmpID',
pm.projectMembersData.EmpID,
false
).EmpFirstName +
' ' +
FormHelperService.getSelectedObject(
pm.resources,
'EmpID',
pm.projectMembersData.EmpID,
false
).EmpLastName;
pm.projectMembersData.ReadText = projectConstants.PERMISSION_TYPE_OBJ[
pm.projectMembersData.Read
]
? projectConstants.PERMISSION_TYPE_OBJ[pm.projectMembersData.Read]
: '--';
pm.projectMembersData.WriteText = projectConstants.PERMISSION_TYPE_OBJ[
pm.projectMembersData.Write
]
? projectConstants.PERMISSION_TYPE_OBJ[pm.projectMembersData.Write]
: '--';
// pm.projectMembersData.ProjectsInvolved = 'OSMOSYS';
if ($scope.isProjectEdit) {
membersEditProjectForm();
} else {
memberNewProjectForm($event);
}
};
/**
* Function to be executed in the new project page.
* #param {*} event
*/
function memberNewProjectForm($event) {
// Setting this value at the time of clicking on edit button.
if (pm.isProjectMemberEdit) {
oldProjectMemberData = {};
angular.extend(
pm.projectMemberList[currentIndex],
pm.projectMembersData
);
pm.pmFormBtn = $translate.instant('btn.add');
} else {
pm.projectMembersData.CreatedOn =
DateHelperService.setTimeFormatString(new Date());
pm.projectMemberList.splice(0, 0, pm.projectMembersData);
// remove that element from employees dropdown to not to select it again
enableOrDisableEmployee(pm.projectMembersData.EmpID, true);
}
angular.element($event.target).data('isInvalid', true);
$scope.showNoErrorsPerm = true;
pm.projectMembersData = {
EmpID: '',
Read: roleConstants.NONE,
Write: roleConstants.NONE,
IsProjectManager: false,
ProjectNames: ''
};
pm.bindDataToGrid(!pm.isProjectMemberEdit, pm.projectMemberList);
$scope.updateProjectMembers({ projectMembers: pm.projectMemberList });
pm.isProjectMemberEdit = false;
}
/**
* Function to add a new project member
* #param {*} data
* #param {*} projectMemberDetails
*/
function addProjectMember(data, projectMemberDetails) {
ProjectService.addProjectMember(data).then(function (response) {
if (response.isValid()) {
pm.projectMemberList.splice(0, 0, pm.projectMembersData);
pm.bindDataToGrid(false, pm.projectMemberList);
enableOrDisableEmployee(pm.projectMembersData.EmpID, true);
resetPMForm();
$scope.$emit('evt.update-project-managers', projectMemberDetails);
logger.success('Resource has been added to the project');
} else {
var errorMsg = response.getMessage(
$translate.instant('prj.resource'),
$translate.instant('event.adding')
);
logger.error(errorMsg);
}
});
}
/**
* Function to update a project member's details
* #param {*} data
* #param {*} projectMemberDetails
*/
function updateProjectMember(data, projectMemberDetails) {
ProjectService.updateProjectMember(data).then(function (response) {
if (response.isValid()) {
angular.extend(
pm.projectMemberList[currentIndex],
pm.projectMembersData
);
pm.pmFormBtn = $translate.instant('btn.add');
pm.bindDataToGrid(pm.isProjectMemberEdit, pm.projectMemberList);
enableOrDisableEmployee(pm.projectMembersData.EmpID, true);
$scope.$emit('evt.update-project-managers', projectMemberDetails);
resetPMForm();
pm.isProjectMemberEdit = false;
logger.success("Project member's details have been updated.");
} else {
var errorMsg = response.getMessage(
$translate.instant('prj.project_members_details'),
$translate.instant('event.updating')
);
logger.error(errorMsg);
}
});
}
/**
* Function to be executed in the project details form
*/
function membersEditProjectForm() {
var projectMemberDetails = {
empID: pm.projectMembersData.EmpID,
name: pm.projectMembersData.ProjectMember,
isProjectManager: pm.projectMembersData.IsProjectManager,
};
var members = [];
// Adding the projectCode to project member's data
pm.projectMembersData.ProjectCode = $stateParams.projectID;
// Creating the UserID property as the add and update project members API methods expects the UserID and not
// the EmpID
pm.projectMembersData.UserID = pm.projectMembersData.EmpID;
// Adding the current project memeber to the members array as the API methods for the
// add and update project member expects an array
members.push(pm.projectMembersData);
if (pm.isProjectMemberEdit) {
updateProjectMember(members, projectMemberDetails);
} else {
addProjectMember(members, projectMemberDetails);
}
}
pm.eleFocus = function () {
FormHelperService.searchElementFocus();
};
// Clearing the searched item when closing the dropdown
pm.clearSearchTerm = function () {
pm.projectMemberSearchText = '';
};
/**
* return the search results for auto complete controls.
*/
pm.querySearch = function (
query,
list,
listObjProp,
listObjPropName,
splitObjProp
) {
if (!list || list === '') {
list = [];
}
if (listObjPropName) {
var queryObj = {};
queryObj[listObjPropName] = query;
if (splitObjProp) {
var listOjProp = listObjProp.split('.');
pm[listOjProp[0]][listOjProp[1]] = $filter('filter')(
list,
queryObj
);
} else {
pm[listObjProp] = $filter('filter')(list, queryObj);
}
} else {
pm[listObjProp] = $filter('filter')(list, query);
}
};
// Fetching the row and row index when clicking on edit button and showing the details in form
angular.element('table').on('click', '.edit-project-member', function () {
pm.projectMembersData = $scope.dtApiProjectMembers.getRowData(
this.parentElement
);
pm.IsProjectManager = pm.projectMembersData.IsProjectManager;
if (pm.projectMembersData.EmpCostPerHour) {
pm.projectMembersData.EmpCostPerHour =
typeof pm.projectMembersData.EmpCostPerHour === 'string'
? pm.projectMembersData.EmpCostPerHour
: pm.projectMembersData.EmpCostPerHour.toFixed(2);
}
if (pm.projectMembersData.CostPerHour) {
pm.projectMembersData.CostPerHour =
typeof pm.projectMembersData.CostPerHour === 'string'
? pm.projectMembersData.CostPerHour
: pm.projectMembersData.CostPerHour.toFixed(2);
}
// Adding the code as IsProjectManager property is mandatory and sometimes may not be defined
if (!pm.projectMembersData.IsProjectManager) {
pm.projectMembersData.IsProjectManager = false;
}
angular.copy(pm.projectMembersData, oldProjectMemberData);
pm.isProjectMemberEdit = true;
pm.pmFormBtn = $translate.instant('btn.update');
currentIndex = $scope.dtApiProjectMembers.getRowIndex(
this.parentElement
);
});
// Showing an alert and Deleting the row data
angular
.element('table')
.on('click', '.delete-project-member', function () {
var deleteRowIndex = $scope.dtApiProjectMembers.getRowIndex(
this.parentElement
);
var deleteRowData = $scope.dtApiProjectMembers.getRowData(
this.parentElement
);
showConfirmDialog(deleteRowIndex, deleteRowData);
});
function showConfirmDialog(deleteRowIndex, deleteRowData) {
var confirm = $mdDialog
.confirm({
onComplete: function afterShowAnimation() {
var $dialog = angular.element(
document.querySelector('md-dialog')
);
var $dialogContent = $dialog.find('md-dialog-content');
var $actionsSection = $dialog.find('md-dialog-actions');
var $cancelButton = $actionsSection.children()[0];
var $confirmButton = $actionsSection.children()[1];
angular.element($dialogContent).addClass('custom-confirm-class');
angular.element($confirmButton).addClass('md-raised md-warn');
angular.element($cancelButton).addClass('md-raised');
},
})
.title($translate.instant('prj.confirm_pm_delete'))
.textContent(
'Are you sure you want to delete the selected project member?' +
deleteRowData.ProjectMember
)
.ariaLabel($translate.instant('prj.confirm_pm_delete'))
.ok($translate.instant('bar.delete'))
.cancel($translate.instant('bar.cancel'));
$mdDialog.show(confirm).then(function () {
pm.isProjectMemberEdit = false;
if ($scope.isProjectEdit) {
var projectMemberData = [
{
ProjectCode: $stateParams.projectID,
UserID: deleteRowData.EmpID,
},
];
ProjectService.deleteProjectMember(projectMemberData).then(
function (response) {
if (response.isValid()) {
logger.success(
$translate.instant('prj.project_member_removed_success_msg')
);
deleteProjectMember(deleteRowIndex, deleteRowData);
if (deleteRowData.IsProjectManager) {
$scope.$emit(
'evt.project-manager-removed',
deleteRowData.EmpID.toString()
);
}
} else {
var errorMsg = response.getMessage(
$translate.instant('prj.project_member'),
$translate.instant('event.deleting')
);
logger.error(errorMsg);
}
}
);
} else {
deleteProjectMember(deleteRowIndex, deleteRowData);
$scope.updateProjectMembers({
projectMembers: pm.projectMemberList,
});
}
pm.pmFormBtn = $translate.instant('btn.save');
pm.projectMembersData = {
EmpID: '',
Read: roleConstants.NONE,
Write: roleConstants.NONE,
IsProjectManager: false,
};
});
}
function deleteProjectMember(deleteRowIndex, deleteRowData) {
pm.projectMemberList.splice(deleteRowIndex, 1);
pm.bindDataToGrid(false, pm.projectMemberList);
var oldEmpId = deleteRowData.EmpID;
// Add that element to employees dropdown to be able to select it again
enableOrDisableEmployee(oldEmpId, false);
}
function enableOrDisableEmployee(empId, isEnable) {
for (var j = 0; j < pm.resources.length; j++) {
if (pm.resources[j].EmpID === empId) {
pm.resources[j].isDisabled = isEnable;
return;
}
}
}
pm.isReadPermissionLevelLess = function (level) {
return pm.projectMembersData.Read < level ? true : false;
};
/**
* To change the write permission default value based on the read permission
* Ex : If read - own, then write should not be all
*/
pm.changeWriteDefaultValue = function () {
if (pm.projectMembersData.Read < pm.projectMembersData.Write) {
pm.projectMembersData.Write = pm.projectMembersData.Read;
}
};
/**
* Get the cost per hour of an employee when the employee value is changed
*/
pm.getCostPerHour = function () {
ResourceService.getEmployeeCostPerHour(
pm.projectMembersData.EmpID
).then(
function (response) {
if (response.isValid()) {
var costPerHour = response.getData();
if (costPerHour && costPerHour[0]) {
pm.projectMembersData.EmpCostPerHour =
costPerHour[0].CostPerHour.toFixed(2);
pm.projectMembersData.CostPerHour =
pm.projectMembersData.EmpCostPerHour;
}
}
},
function (error) {
logger.error(error);
}
);
};
$scope.$on("evt.clear-pm-members-grids-data", function () {
pm.projectMemberList = [];
pm.bindDataToGrid(false, pm.projectMemberList);
});
// Binding the data to grid with updated data.
pm.bindDataToGrid = function (reOrder, projectMembers) {
pm.projectMemberList = projectMembers;
if (pm.projectMemberList && pm.projectMemberList.length) {
var currPage = $scope.dtApiProjectMembers.getPageInfo()['page'];
$scope.dtApiProjectMembers.bindData(pm.projectMemberList);
currPage = currPage >= 0 ? currPage : 0;
if (reOrder) {
$scope.dtApiProjectMembers.orderByColumn([[1, 'desc']]);
} else {
$scope.dtApiProjectMembers.setPage(currPage);
}
} else {
$scope.dtApiProjectMembers.bindData([]);
}
};
function resetPMForm() {
$scope.showNoErrorsPerm = true;
$scope.vm.pmData.$setPristine();
$scope.vm.pmData.$setUntouched(true);
pm.projectMembersData = {
EmpID: '',
Read: roleConstants.NONE,
Write: roleConstants.NONE,
IsProjectManager: false,
};
}
$scope.$on('evt.reset-project-members-form', function () {
resetPMForm();
});
}
);
I tried linking data when adding new resource- I can see project getting linked. I hard coded this to see if I was working on the right container.

Isotope: Combined multiple checkbox and searchbox filtering

I'm trying to combine the Isotope multiple checkbox filtering with a searchbox.
I used the example with the checkbox filters from here and tried to implement the searchbox but with no luck.
Just the checkbox filtering works well. I think i'm close to the solution but my javascript skills are at a very beginner level.
I commented out the section of what i've tried to implement.
Thank you for some hints
// quick search regex
var qsRegex;
var $grid;
var filters = {};
var $grid = $('.grid');
//set initial options
$grid.isotope({
layoutMode: 'fitRows'
});
$(function() {
$grid = $('#grid');
$grid.isotope();
// do stuff when checkbox change
$('#options').on('change', function(jQEvent) {
var $checkbox = $(jQEvent.target);
manageCheckbox($checkbox);
var comboFilter = getComboFilter(filters);
/*var searchResult = qsRegex ? $(this).text().match(qsRegex) : true;
var filterResult = function() {
return comboFilter && searchResult;
}*/
$grid.isotope({
filter: comboFilter //or filterResult
});
});
});
function getComboFilter(filters) {
var i = 0;
var comboFilters = [];
var message = [];
for (var prop in filters) {
message.push(filters[prop].join(' '));
var filterGroup = filters[prop];
// skip to next filter group if it doesn't have any values
if (!filterGroup.length) {
continue;
}
if (i === 0) {
// copy to new array
comboFilters = filterGroup.slice(0);
} else {
var filterSelectors = [];
// copy to fresh array
var groupCombo = comboFilters.slice(0); // [ A, B ]
// merge filter Groups
for (var k = 0, len3 = filterGroup.length; k < len3; k++) {
for (var j = 0, len2 = groupCombo.length; j < len2; j++) {
filterSelectors.push(groupCombo[j] + filterGroup[k]); // [ 1, 2 ]
}
}
// apply filter selectors to combo filters for next group
comboFilters = filterSelectors;
}
i++;
}
var comboFilter = comboFilters.join(', ');
return comboFilter;
}
// use value of search field to filter
var $quicksearch = $('.quicksearch').keyup(debounce(function() {
qsRegex = new RegExp($quicksearch.val(), 'gi');
$grid.isotope();
}, ));
// debounce so filtering doesn't happen every millisecond
function debounce(fn, threshold) {
var timeout;
threshold = threshold || 100;
return function debounced() {
clearTimeout(timeout);
var args = arguments;
var _this = this;
function delayed() {
fn.apply(_this, args);
}
timeout = setTimeout(delayed, threshold);
}
}
function manageCheckbox($checkbox) {
var checkbox = $checkbox[0];
var group = $checkbox.parents('.option-set').attr('data-group');
// create array for filter group, if not there yet
var filterGroup = filters[group];
if (!filterGroup) {
filterGroup = filters[group] = [];
}
var isAll = $checkbox.hasClass('all');
// reset filter group if the all box was checked
if (isAll) {
delete filters[group];
if (!checkbox.checked) {
checkbox.checked = 'checked';
}
}
// index of
var index = $.inArray(checkbox.value, filterGroup);
if (checkbox.checked) {
var selector = isAll ? 'input' : 'input.all';
$checkbox.siblings(selector).prop('checked', false);
if (!isAll && index === -1) {
// add filter to group
filters[group].push(checkbox.value);
}
} else if (!isAll) {
// remove filter from group
filters[group].splice(index, 1);
// if unchecked the last box, check the all
if (!$checkbox.siblings('[checked]').length) {
$checkbox.parents('.option-set').find(selector).prop('checked', false);
}
}
I found the solution by myself, but i had to add a second function for returning the searchresult. Otherwise the search function is triggered only after using a checkbox or leaving the search box input field.
How could i avoid this redundand code?
JS:
// use value of search field to filter
var $quicksearch = $('.quicksearch').keyup(debounce(function() {
qsRegex = new RegExp($quicksearch.val(), 'gi');
$grid.isotope();
}, 200));
$(function() {
$grid = $('#grid');
$grid.isotope({
filter: function() {
var searchResult = qsRegex ? $(this).text().match(qsRegex) : true;
return searchResult;
}
});
// do stuff when checkbox change
$('#options').on('change', function(jQEvent) {
var $checkbox = $(jQEvent.target);
manageCheckbox($checkbox);
var comboFilter = getComboFilter(filters);
$grid.isotope({
filter: function() {
var buttonResult = comboFilter ? $(this).is(comboFilter) : true;
var searchResult = qsRegex ? $(this).text().match(qsRegex) : true;
return buttonResult && searchResult;
}
});
});
});

Buffering in jPlayer

We use Icecast streaming server to jPlayer on our website and also used on our mobile app. I was trying to add an <intro> to the Icecast config, but when I do, it presents an issue on mobile devices. Whenever the phone has an interruption causing a temporary disconnect, like a call that comes in, the stream repeats what you started listening to when you first connected to the stream, after the intro plays again of course. For instance, if I start the stream listening to one show or song, a call comes in and ends, the intro plays on the reconnect and the stream plays from where I initially started listening.
I have played with Icecast queue and burst settings up and down and none at all, and tried different formats, the same result. I've also had conversations on a couple of other streaming related posts and have been told it seems the issue is with the client buffer and player, which I did not set up. I took a look at our stream-player.js, it is jPlayer 2.9.2 with the following tacked on to the end at line 3507:
;(function() {
var DOMParser, find, parse;
DOMParser = (typeof window !== "undefined" && window !== null ? window.DOMParser : void 0) || (typeof require === "function" ? require('xmldom').DOMParser : void 0) || function() {};
find = function(node, list) {
var attributes, childNode, childNodeName, childNodes, i, match, x, _i, _j, _ref, _ref1;
if (node.hasChildNodes()) {
childNodes = node.childNodes;
for (i = _i = 0, _ref = childNodes.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
childNode = childNodes[i];
childNodeName = childNode.nodeName;
if (/REF/i.test(childNodeName)) {
attributes = childNode.attributes;
for (x = _j = 0, _ref1 = attributes.length; 0 <= _ref1 ? _j < _ref1 : _j > _ref1; x = 0 <= _ref1 ? ++_j : --_j) {
match = attributes[x].nodeName.match(/HREF/i);
if (match) {
list.push({
file: childNode.getAttribute(match[0]).trim()
});
break;
}
}
} else if (childNodeName !== '#text') {
find(childNode, list);
}
}
}
return null;
};
parse = function(playlist) {
var doc, ret;
ret = [];
doc = (new DOMParser()).parseFromString(playlist, 'text/xml').documentElement;
if (!doc) {
return ret;
}
find(doc, ret);
return ret;
};
(typeof module !== "undefined" && module !== null ? module.exports : window).ASX = {
name: 'asx',
parse: parse
};
}).call(this);
(function() {
var COMMENT_RE, EXTENDED, comments, empty, extended, parse, simple;
EXTENDED = '#EXTM3U';
COMMENT_RE = /:(?:(-?\d+),(.+)\s*-\s*(.+)|(.+))\n(.+)/;
extended = function(line) {
var match;
match = line.match(COMMENT_RE);
if (match && match.length === 6) {
return {
length: match[1] || 0,
artist: match[2] || '',
title: match[4] || match[3],
file: match[5].trim()
};
}
};
simple = function(string) {
return {
file: string.trim()
};
};
empty = function(line) {
return !!line.trim().length;
};
comments = function(line) {
return line[0] !== '#';
};
parse = function(playlist) {
var firstNewline;
playlist = playlist.replace(/\r/g, '');
firstNewline = playlist.search('\n');
if (playlist.substr(0, firstNewline) === EXTENDED) {
return playlist.substr(firstNewline).split('\n#').filter(empty).map(extended);
} else {
return playlist.split('\n').filter(empty).filter(comments).map(simple);
}
};
(typeof module !== "undefined" && module !== null ? module.exports : window).M3U = {
name: 'm3u',
parse: parse
};
}).call(this);
(function() {
var LISTING_RE, parse;
LISTING_RE = /(file|title|length)(\d+)=(.+)\r?/i;
parse = function(playlist) {
var index, key, line, match, tracks, value, _, _i, _len, _ref;
tracks = [];
_ref = playlist.trim().split('\n');
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
line = _ref[_i];
match = line.match(LISTING_RE);
if (match && match.length === 4) {
_ = match[0], key = match[1], index = match[2], value = match[3];
if (!tracks[index]) {
tracks[index] = {};
}
tracks[index][key.toLowerCase()] = value;
}
}
return tracks.filter(function(track) {
return track != null;
});
};
(typeof module !== "undefined" && module !== null ? module.exports : window).PLS = {
name: 'pls',
parse: parse
};
}).call(this);
;(function() {
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
window.PlayerUI = (function() {
function PlayerUI(container) {
var _this = this;
this.container = container;
this.onStateButtonClicked = __bind(this.onStateButtonClicked, this);
this.duration = null;
this.state = 'loading';
this.player = $('<div></div>');
this.container.append(this.player);
this.player.jPlayer({
ready: function() {
return _this.state = 'paused';
}
});
this.volume = this.container.find('.volume-slider input').rangeslider({
polyfill: false,
onSlide: function(position, value) {
return _this.player.jPlayer('volume', value / 100.0);
},
onSlideEnd: function(position, value) {
return _this.player.jPlayer('volume', value / 100.0);
}
});
this.hookEvents();
}
PlayerUI.prototype.hookEvents = function() {
var _this = this;
this.container.find('.state-button a').click(this.onStateButtonClicked);
this.player.on($.jPlayer.event.play, function() {
return _this.setState('playing');
});
this.player.on($.jPlayer.event.pause, function() {
return _this.setState('paused');
});
this.player.on($.jPlayer.event.durationchange, function(e) {
return _this.container.trigger('player.setProgressMax', {
maxValue: e.jPlayer.status.duration
});
});
this.player.on($.jPlayer.event.timeupdate, function(e) {
return _this.container.trigger('player.updateProgress', {
value: e.jPlayer.status.currentTime
});
});
return this.player.on($.jPlayer.event.ended, function(e) {
return _this.container.trigger('player.trackEnded');
});
};
PlayerUI.prototype.setState = function(state) {
this.state = state;
return this.container.find('.state-button a').removeClass().addClass("state-" + state);
};
PlayerUI.prototype.onStateButtonClicked = function(event) {
event.preventDefault();
switch (this.state) {
case 'playing':
return this.pause();
case 'paused':
return this.play();
default:
return this.noop();
}
};
PlayerUI.prototype.setMedia = function(media) {
this.pause();
return this.player.jPlayer('setMedia', media);
};
PlayerUI.prototype.setProgress = function(pct) {
return this.player.jPlayer('playHead', pct);
};
PlayerUI.prototype.play = function() {
this.setState('playing');
return this.player.jPlayer('play');
};
PlayerUI.prototype.pause = function() {
this.setState('paused');
return this.player.jPlayer('pause');
};
PlayerUI.prototype.noop = function() {
return null;
};
return PlayerUI;
})();
}).call(this);
;(function() {
window.PlaylistUI = (function() {
function PlaylistUI(container) {
var _this = this;
this.container = container;
this.container.hide();
$(window).on('playlistloader.finished', function(evt, data) {
return _this.setPlaylist(PlaylistLoader.coalescePlaylists(data.playlists));
});
}
PlaylistUI.prototype.loadM3UList = function(m3uList) {
return new PlaylistLoader(m3uList);
};
PlaylistUI.prototype.setPlaylist = function(playlistData) {
if (typeof playlistData.data !== 'undefined') {
this.name = playlistData.name;
playlistData = playlistData.data;
}
this.playlist = playlistData;
this.container.hide();
this.unhookEvents();
this.renderPlaylist();
this.container.show();
this.hookEvents();
return this.container.trigger('playlistui.ready', {
ui: this,
autoplay: false //this.getAutoplay()
});
};
PlaylistUI.prototype.unhookEvents = function() {
return this.container.find('.playlist-item').off('click.playlistUI', 'a');
};
PlaylistUI.prototype.hookEvents = function() {
var _this = this;
return this.container.find('.playlist-item').on('click.playlistUI', 'a', function(evt) {
var idx, item;
evt.preventDefault();
idx = $(evt.target).parent('.playlist-item').data('idx');
item = _this.getItemByIdx(idx);
return _this.select(item);
});
};
PlaylistUI.prototype.renderPlaylist = function() {
var idx, item, playlist, _i, _len, _ref, _results;
playlist = this.container.find('.playlist');
playlist.empty();
_ref = this.playlist;
_results = [];
for (idx = _i = 0, _len = _ref.length; _i < _len; idx = ++_i) {
item = _ref[idx];
_results.push(playlist.append(this.rowTemplate(item, idx)));
}
return _results;
};
PlaylistUI.prototype.rowTemplate = function(item, idx) {
return $("<li class=\"playlist-item\" data-idx=\"" + idx + "\">" + item.title + "</li>");
};
PlaylistUI.prototype.getAutoplay = function() {
var item, _i, _len, _ref;
_ref = this.playlist;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
item = _ref[_i];
if (item.autoplay) {
return item;
}
}
return null;
};
PlaylistUI.prototype.getItemByIdx = function(idx) {
return this.playlist[idx];
};
PlaylistUI.prototype.getRowForItem = function(item) {
var compare, found, idx, _i, _len, _ref;
_ref = this.playlist;
for (idx = _i = 0, _len = _ref.length; _i < _len; idx = ++_i) {
compare = _ref[idx];
if (compare === item) {
found = this.container.find(".playlist-item[data-idx=" + idx + "]");
return found;
}
}
return null;
};
PlaylistUI.prototype.getIndexForItem = function(item) {
var compare, idx, _i, _len, _ref;
_ref = this.playlist;
for (idx = _i = 0, _len = _ref.length; _i < _len; idx = ++_i) {
compare = _ref[idx];
if (item === compare) {
return idx;
}
}
return null;
};
PlaylistUI.prototype.findNext = function() {
var currentIndex, nextIndex;
currentIndex = this.getIndexForItem(this.current);
if (currentIndex === null) {
return null;
}
nextIndex = currentIndex + 1;
if (nextIndex >= this.playlist.length) {
return null;
}
return this.playlist[nextIndex];
};
PlaylistUI.prototype.select = function(item) {
if (item) {
this.current = item;
this.getRowForItem(item).addClass('selected').siblings().removeClass('selected');
return this.container.trigger('playlistui.select', {
ui: this,
item: item
});
}
};
PlaylistUI.prototype.selectFirst = function() {
return this.select(this.playlist[0]);
};
PlaylistUI.prototype.selectNext = function() {
var nextItem;
nextItem = this.findNext();
if (nextItem === null) {
return false;
}
this.select(nextItem);
return true;
};
return PlaylistUI;
})();
}).call(this);
;(function() {
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
window.PlaylistLoader = (function() {
function PlaylistLoader(playlists) {
this.playlists = playlists;
this.loadedItem = __bind(this.loadedItem, this);
this.loadPlaylists();
}
PlaylistLoader.prototype.loadPlaylists = function() {
var idx, item, _i, _len, _ref, _results,
_this = this;
this.loadCount = 0;
this.data = new Array(this.playlists.length);
_ref = this.playlists;
_results = [];
for (idx = _i = 0, _len = _ref.length; _i < _len; idx = ++_i) {
item = _ref[idx];
_results.push((function() {
var tmp;
tmp = idx;
return jQuery.ajax({
url: item
}).done(function(data) {
return _this.loadedItem(tmp, data);
});
})());
}
return _results;
};
PlaylistLoader.prototype.loadedItem = function(idx, data) {
var playlist;
playlist = M3U.parse(data);
this.data[idx] = playlist;
$(window).trigger('playlistloader.loadeditem', {
index: idx,
playlist: playlist
});
this.loadCount++;
if (this.loadCount === this.playlists.length) {
return this.finishedLoading();
}
};
PlaylistLoader.prototype.finishedLoading = function() {
return $(window).trigger('playlistloader.finished', {
playlists: this.data
});
};
PlaylistLoader.coalescePlaylists = function(playlistsLoaded) {
var fileEntry, output, playlist, _i, _j, _len, _len1;
output = [];
for (_i = 0, _len = playlistsLoaded.length; _i < _len; _i++) {
playlist = playlistsLoaded[_i];
for (_j = 0, _len1 = playlist.length; _j < _len1; _j++) {
fileEntry = playlist[_j];
output.push(fileEntry);
}
}
return output;
};
return PlaylistLoader;
})();
}).call(this);
;(function() {
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
window.StreamUI = (function() {
function StreamUI(selector, streamPlaylists) {
this.selector = selector;
this.streamPlaylists = streamPlaylists;
this.playlistSelect = __bind(this.playlistSelect, this);
this.playlistReady = __bind(this.playlistReady, this);
this.container = jQuery(this.selector);
this.playlist = new PlaylistUI(this.container.find('.playlist-ui'));
this.player = new PlayerUI(this.container.find('.player-ui'));
this.hookEvents();
this.playlist.loadM3UList(this.streamPlaylists);
}
StreamUI.prototype.hookEvents = function() {
var playlistUI;
playlistUI = this.container.find('.playlist-ui');
playlistUI.on('playlistui.ready', this.playlistReady);
return playlistUI.on('playlistui.select', this.playlistSelect);
};
StreamUI.prototype.playlistReady = function(evt, eventinfo) {
if (eventinfo.autoplay !== null) {
return eventinfo.ui.select(eventinfo.autoplay);
} else {
return eventinfo.ui.selectFirst();
}
};
StreamUI.prototype.playlistSelect = function(evt, eventinfo) {
this.player.setMedia({
mp3: eventinfo.item.file
});
return this.player.play();
};
return StreamUI;
})();
}).call(this);
Although I'm primarily a linux developer with most of my programming experience in Perl and PHP, and do know jQuery pretty well dealing with my web development, I'm surely a novice when it comes to jPlayer or even audio streaming. I was hoping someone could spot something in hte code above that could contribute to the issue we have when adding an intro to our Icecast 2.4.4 stream?
Our streams are available at the URL below, I have the intro on our HD4 stream at the moment.
streaming player
The issue is easily duplicated by starting the stream and listening a bit until the song changes, call the phone letting it interrupt the stream, then hang up. This will cause the first song listened to be playing again after the intro.
I believe the codec is a match, I did have an issue getting the intro to work until I formatted as MP3 128Kbps bit rate 44.1KHz sampling and 2 channel stereo. Here is the intro file info:
user#stream:~$ mediainfo /usr/share/icecast2/web/high_quality.mp3
General
Complete name : /usr/share/icecast2/web/high_quality.mp3
Format : MPEG Audio
File size : 138 KiB
Duration : 8s 777ms
Overall bit rate mode : Constant
Overall bit rate : 128 Kbps
Writing library : LAME3.99r
Audio
Format : MPEG Audio
Format version : Version 1
Format profile : Layer 3
Mode : Joint stereo
Mode extension : MS Stereo
Duration : 8s 803ms
Bit rate mode : Constant
Bit rate : 128 Kbps
Channel(s) : 2 channels
Sampling rate : 44.1 KHz
Compression mode : Lossy
Stream size : 137 KiB (100%)
Writing library : LAME3.99r
Encoding settings : -m j -V 4 -q 3 -lowpass 17 -b 128
Sounds like the underlying browser cache kicks in and forces replay of something held in memory. Browsers are 'awesome' like that under some circumstances and will then go out of their way to ignore no-cache directives and other things.
One way to make sure the browser doesn't try to play cache shenanigans is to add a "cache buster". Essentially a query string ( /stream?foo=bar ), which makes the browser engine think it's dynamically generated content and discard its cache; cf. https://www.keycdn.com/support/what-is-cache-busting .
At this time your Icecast server doesn't seem to answer any requests. So I can't look into the specifics on your side.

Jump(seek) to markers in videojs on button click

I have added markers to my videojs player. I want to jump to start time of every marker every time I click a particular button (say a next button). How should I do this. I know I have to change the currentTime but I am not getting how to solve the complete problem. I have start time of each marker. Any kind of input would be helpful.
Player.js
import assign from 'object-assign'
import cx from 'classnames'
import blacklist from 'blacklist'
import React from 'react'
module.exports = React.createClass({
displayName: 'VideoJS',
componentDidMount() {
var self = this;
var player = videojs(this.refs.video, this.props.options).ready(function() {
self.player = this;
self.player.on('play', self.handlePlay);
});
if(this.props.onPlayerInit) this.props.onPlayerInit(player);
player.markers({
markerStyle: {},
markers: [
{ startTime:10, endTime:15, time: 9.5, text: "compliance"},
{ startTime:20, endTime:25, time: 16, text: "compliance"},
{ startTime:30, endTime:38, time: 23.6,text: "compliance"},
{ startTime:51, endTime:55, time: 28, text: "compliance"}
]
});
},
handlePlay: function() {
if(this.props.onPlay) {
this.props.onPlay(this.player);
}
},
render() {
var props = blacklist(this.props, 'children', 'className', 'src', 'type', 'onPlay');
props.className = cx(this.props.className, 'videojs', 'video-js vjs-default-skin', 'vjs-big-play-centered');
assign(props, {
ref: 'video',
controls: true
});
return (
<div>
<video {... props}>
<source src={this.props.src} type={this.props.type}/>
</video>
</div>
)
}
});
Marker.js
(function($, video, undefined) {
//default setting
var defaultSetting = {
markerStyle: {
'border-radius': '0%',
},
markerTip: {
display: true,
text: function(marker) {
return "Break: "+ marker.text;
},
time: function(marker) {
return marker.time;
}
},
breakOverlay:{
display: false,
displayTime: 3,
text: function(marker) {
return "Break overlay: " + marker.overlayText;
},
style: {
'width':'100%',
'height': '20%',
'background-color': 'rgba(0,0,0,0.7)',
'color': 'white',
'font-size': '17px'
}
},
onMarkerClick: function(marker) {},
onMarkerReached: function(marker) {},
markers: []
};
// create a non-colliding random number
function generateUUID() {
var d = new Date().getTime();
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = (d + Math.random()*16)%16 | 0;
d = Math.floor(d/16);
return (c=='x' ? r : (r&0x3|0x8)).toString(16);
});
return uuid;
};
function registerVideoJsMarkersPlugin(options) {
/**
* register the markers plugin (dependent on jquery)
*/
var setting = $.extend(true, {}, defaultSetting, options),
markersMap = {},
markersList = [], // list of markers sorted by time
videoWrapper = $(this.el()),
currentMarkerIndex = -1,
player = this,
markerTip = null,
breakOverlay = null,
overlayIndex = -1;
function sortMarkersList() {
// sort the list by time in asc order
markersList.sort(function(a, b){
return setting.markerTip.time(a) - setting.markerTip.time(b);
});
}
function addMarkers(newMarkers) {
// create the markers
$.each(newMarkers, function(index, marker) {
//console.log(index);
// console.log(marker);
marker.key = generateUUID();
//console.log(marker.key);
videoWrapper.find('.vjs-progress-control').append(
createMarkerDiv(marker));
// store marker in an internal hash map
markersMap[marker.key] = marker;
console.log(markersMap);
markersList.push(marker);
});
sortMarkersList();
}
function getPosition(marker){
return (setting.markerTip.time(marker) / player.duration()) * 100
}
function createMarkerDiv(marker, duration) {
var markerDiv = $("<div class='vjs-marker'></div>");
console.log(marker.length);
markerDiv.css(setting.markerStyle)
.css({"margin-left" : -parseFloat(markerDiv.css("width"))/2 + 'px',
"left" : getPosition(marker) + '%', })
.attr("data-marker-key", marker.key)
.attr("data-marker-time", setting.markerTip.time(marker));
console.log(setting.markerTip.time(marker));
// add user-defined class to marker
if (marker.class) {
markerDiv.addClass(marker.class);
}
// bind click event to seek to marker time
markerDiv.on('click', function(e) {
var preventDefault = false;
if (typeof setting.onMarkerClick === "function") {
// if return false, prevent default behavior
preventDefault = setting.onMarkerClick(marker) == false;
}
if (!preventDefault) {
var key = $(this).data('marker-key');
player.currentTime(setting.markerTip.time(markersMap[key]));
}
});
if (setting.markerTip.display) {
registerMarkerTipHandler(markerDiv);
}
return markerDiv;
}
function updateMarkers() {
// update UI for markers whose time changed
for (var i = 0; i< markersList.length; i++) {
var marker = markersList[i];
var markerDiv = videoWrapper.find(".vjs-marker[data-marker-key='" + marker.key +"']");
var markerTime = setting.markerTip.time(marker);
if (markerDiv.data('marker-time') != markerTime) {
markerDiv.css({"left": getPosition(marker) + '%'})
.attr("data-marker-time", markerTime);
}
}
sortMarkersList();
}
function removeMarkers(indexArray) {
// reset overlay
if (breakOverlay){
overlayIndex = -1;
breakOverlay.css("visibility", "hidden");
}
currentMarkerIndex = -1;
for (var i = 0; i < indexArray.length; i++) {
var index = indexArray[i];
var marker = markersList[index];
if (marker) {
// delete from memory
delete markersMap[marker.key];
markersList[index] = null;
// delete from dom
videoWrapper.find(".vjs-marker[data-marker-key='" + marker.key +"']").remove();
}
}
// clean up array
for (var i = markersList.length - 1; i >=0; i--) {
if (markersList[i] === null) {
markersList.splice(i, 1);
}
}
// sort again
sortMarkersList();
}
// attach hover event handler
function registerMarkerTipHandler(markerDiv) {
markerDiv.on('mouseover', function(){
var marker = markersMap[$(this).data('marker-key')];
markerTip.find('.vjs-tip-inner').text(setting.markerTip.text(marker));
// margin-left needs to minus the padding length to align correctly with the marker
markerTip.css({"left" : getPosition(marker) + '%',
"margin-left" : -parseFloat(markerTip.css("width"))/2 - 5 + 'px',
"visibility" : "visible"});
}).on('mouseout',function(){
markerTip.css("visibility", "hidden");
});
}
function initializeMarkerTip() {
markerTip = $("<div class='vjs-tip'><div class='vjs-tip-arrow'></div><div class='vjs-tip-inner'></div></div>");
videoWrapper.find('.vjs-progress-control').append(markerTip);
}
// show or hide break overlays
function updateBreakOverlay() {
if(!setting.breakOverlay.display || currentMarkerIndex < 0){
return;
}
var currentTime = player.currentTime();
var marker = markersList[currentMarkerIndex];
var markerTime = setting.markerTip.time(marker);
if (currentTime >= markerTime &&
currentTime <= (markerTime + setting.breakOverlay.displayTime)) {
if (overlayIndex != currentMarkerIndex){
overlayIndex = currentMarkerIndex;
breakOverlay.find('.vjs-break-overlay-text').html(setting.breakOverlay.text(marker));
}
breakOverlay.css('visibility', "visible");
} else {
overlayIndex = -1;
breakOverlay.css("visibility", "hidden");
}
}
// problem when the next marker is within the overlay display time from the previous marker
function initializeOverlay() {
breakOverlay = $("<div class='vjs-break-overlay'><div class='vjs-break-overlay-text'></div></div>")
.css(setting.breakOverlay.style);
videoWrapper.append(breakOverlay);
overlayIndex = -1;
}
function onTimeUpdate() {
onUpdateMarker();
updateBreakOverlay();
}
function onUpdateMarker() {
/*
check marker reached in between markers
the logic here is that it triggers a new marker reached event only if the player
enters a new marker range (e.g. from marker 1 to marker 2). Thus, if player is on marker 1 and user clicked on marker 1 again, no new reached event is triggered)
*/
var getNextMarkerTime = function(index) {
if (index < markersList.length - 1) {
return setting.markerTip.time(markersList[index + 1]);
}
// next marker time of last marker would be end of video time
return player.duration();
}
var currentTime = player.currentTime();
var newMarkerIndex;
if (currentMarkerIndex != -1) {
// check if staying at same marker
var nextMarkerTime = getNextMarkerTime(currentMarkerIndex);
if(currentTime >= setting.markerTip.time(markersList[currentMarkerIndex]) &&
currentTime < nextMarkerTime) {
return;
}
// check for ending (at the end current time equals player duration)
if (currentMarkerIndex === markersList.length -1 &&
currentTime === player.duration()) {
return;
}
}
// check first marker, no marker is selected
if (markersList.length > 0 &&
currentTime < setting.markerTip.time(markersList[0])) {
newMarkerIndex = -1;
} else {
// look for new index
for (var i = 0; i < markersList.length; i++) {
nextMarkerTime = getNextMarkerTime(i);
if(currentTime >= setting.markerTip.time(markersList[i]) &&
currentTime < nextMarkerTime) {
newMarkerIndex = i;
break;
}
}
}
// set new marker index
if (newMarkerIndex != currentMarkerIndex) {
// trigger event
if (newMarkerIndex != -1 && options.onMarkerReached) {
options.onMarkerReached(markersList[newMarkerIndex]);
}
currentMarkerIndex = newMarkerIndex;
}
}
// setup the whole thing
function initialize() {
if (setting.markerTip.display) {
initializeMarkerTip();
}
// remove existing markers if already initialized
player.markers.removeAll();
addMarkers(options.markers);
if (setting.breakOverlay.display) {
initializeOverlay();
}
onTimeUpdate();
player.on("timeupdate", onTimeUpdate);
}
// setup the plugin after we loaded video's meta data
player.on("loadedmetadata", function() {
initialize();
});
// exposed plugin API
player.markers = {
getMarkers: function() {
return markersList;
},
next : function() {
// go to the next marker from current timestamp
var currentTime = player.currentTime();
for (var i = 0; i < markersList.length; i++) {
var markerTime = setting.markerTip.time(markersList[i]);
if (markerTime > currentTime) {
player.currentTime(markerTime);
break;
}
}
},
prev : function() {
// go to previous marker
var currentTime = player.currentTime();
for (var i = markersList.length - 1; i >=0 ; i--) {
var markerTime = setting.markerTip.time(markersList[i]);
// add a threshold
if (markerTime + 0.5 < currentTime) {
player.currentTime(markerTime);
break;
}
}
},
add : function(newMarkers) {
// add new markers given an array of index
addMarkers(newMarkers);
},
remove: function(indexArray) {
// remove markers given an array of index
removeMarkers(indexArray);
},
removeAll: function(){
var indexArray = [];
for (var i = 0; i < markersList.length; i++) {
indexArray.push(i);
}
removeMarkers(indexArray);
},
updateTime: function(){
// notify the plugin to update the UI for changes in marker times
updateMarkers();
},
reset: function(newMarkers){
// remove all the existing markers and add new ones
player.markers.removeAll();
addMarkers(newMarkers);
},
destroy: function(){
// unregister the plugins and clean up even handlers
player.markers.removeAll();
breakOverlay.remove();
markerTip.remove();
player.off("timeupdate", updateBreakOverlay);
delete player.markers;
},
};
}
videojs.plugin('markers', registerVideoJsMarkersPlugin);
})(jQuery, window.videojs);

FuelUX spinbox - use custom strings array

Is it possible to leverage FuelUX spinbox to cycle/scroll through array of custom strings?
This is an example of what I mean, but it's a jQueryUI implementation:
Links to jsfiddle.net must be accompanied by code.
Please indent all code by 4 spaces using the code
toolbar button or the CTRL+K keyboard shortcut.
For more editing help, click the [?] toolbar icon.
http://jsfiddle.net/MartynDavis/gzmvc2ds/
Not out of the box, no.
You'd have to modify this portion of spinbox to make newVal be set by accessing your desired array instead of doing maths:
step: function step(isIncrease) {
//refresh value from display before trying to increment in case they have just been typing before clicking the nubbins
this.setValue(this.getDisplayValue());
var newVal;
if (isIncrease) {
newVal = this.options.value + this.options.step;
} else {
newVal = this.options.value - this.options.step;
}
newVal = newVal.toFixed(5);
this.setValue(newVal + this.unit);
},
Something quick and dirty based on FuelUX spinbox:
/*
* Fuel UX SpinStrings
* https://github.com/ExactTarget/fuelux
*
* Copyright (c) 2014 ExactTarget
* Licensed under the BSD New license.
*/
// -- BEGIN UMD WRAPPER PREFACE --
// For more information on UMD visit:
// https://github.com/umdjs/umd/blob/master/jqueryPlugin.js
(function (factory) {
if (typeof define === 'function' && define.amd) {
// if AMD loader is available, register as an anonymous module.
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// Node/CommonJS
module.exports = factory(require('jquery'));
} else {
// OR use browser globals if AMD is not present
factory(jQuery);
}
}(function ($) {
// -- END UMD WRAPPER PREFACE --
// -- BEGIN MODULE CODE HERE --
var old = $.fn.spinstrings;
// SPINSTRINGS CONSTRUCTOR AND PROTOTYPE
var SpinStrings = function SpinStrings(element, options) {
this.$element = $(element);
this.$element.find('.btn').on('click', function (e) {
//keep spinstrings from submitting if they forgot to say type="button" on their spinner buttons
e.preventDefault();
});
if ($.isPlainObject(options) && 'options' in options) {
if (!$.isArray(options.options)) {
delete options.options;
} else {
options.min = 0;
options.max = options.options.length - 1;
if (options.value && ((idx = options.options.indexOf(options.value)) > -1)) {
options.index = idx;
} else {
options.index = 0;
}
}
}
this.options = $.extend({}, $.fn.spinstrings.defaults, options);
if (this.options.index < this.options.min) {
this.options.index = this.options.min;
} else if (this.options.max < this.options.index) {
this.options.index = this.options.max;
}
this.$input = this.$element.find('.spinstrings-input');
this.$input.on('focusout.fu.spinstrings', this.$input, $.proxy(this.change, this));
this.$element.on('keydown.fu.spinstrings', this.$input, $.proxy(this.keydown, this));
this.$element.on('keyup.fu.spinstrings', this.$input, $.proxy(this.keyup, this));
this.bindMousewheelListeners();
this.mousewheelTimeout = {};
this.$element.on('click.fu.spinstrings', '.spinstrings-up', $.proxy(function () {
this.step(true);
}, this));
this.$element.on('click.fu.spinstrings', '.spinstrings-down', $.proxy(function () {
this.step(false);
}, this));
this.lastValue = this.options.value;
this.render();
if (this.options.disabled) {
this.disable();
}
};
// Truly private methods
var _applyLimits = function(value) {
// if unreadable
if (isNaN(parseFloat(value))) {
return value;
}
// if not within range return the limit
if (value > this.options.max) {
if (this.options.cycle) {
value = this.options.min;
} else {
value = this.options.max;
}
} else if (value < this.options.min) {
if (this.options.cycle) {
value = this.options.max;
} else {
value = this.options.min;
}
}
return value;
};
SpinStrings.prototype = {
constructor: SpinStrings,
destroy: function destroy() {
this.$element.remove();
// any external bindings
// [none]
// set input value attrbute
this.$element.find('input').each(function () {
$(this).attr('value', $(this).val());
});
// empty elements to return to original markup
// [none]
// returns string of markup
return this.$element[0].outerHTML;
},
render: function render() {
this.setValue(this.getDisplayValue());
},
change: function change() {
this.setValue(this.getDisplayValue());
this.triggerChangedEvent();
},
triggerChangedEvent: function triggerChangedEvent() {
var currentValue = this.getValue();
if (currentValue === this.lastValue) return;
this.lastValue = currentValue;
// Primary changed event
this.$element.trigger('changed.fu.spinstrings', currentValue);
},
step: function step(isIncrease) {
//refresh value from display before trying to increment in case they have just been typing before clicking the nubbins
this.setValue(this.getDisplayValue());
var newVal;
if (isIncrease) {
newVal = this.options.index + this.options.step;
} else {
newVal = this.options.index - this.options.step;
}
newVal = _applyLimits.call(this, newVal);
newVal = this.getOptionByIndex(newVal);
this.setValue(newVal);
},
getDisplayValue: function getDisplayValue() {
var inputValue = this.$input.val();
var value = (!!inputValue) ? inputValue : this.options.value;
return value;
},
/**
* #param string value
*/
setDisplayValue: function setDisplayValue(value) {
this.$input.val(value);
},
/**
* #return string
*/
getValue: function getValue() {
return this.options.value;
},
/**
* #param string val
*/
setValue: function setValue(val) {
var intVal = this.getIndexByOption(val);
//cache the pure int value
this.options.value = val;
this.options.index = intVal;
//display number
this.setDisplayValue(val);
return this;
},
value: function value(val) {
if (val || val === 0) {
return this.setValue(val);
} else {
return this.getValue();
}
},
/**
* Get string's position in array of options.
*
* #param string value
* #return integer
*/
getIndexByOption: function(value) {
ret = null;
if (this.options.options) {
ret = this.options.options.indexOf(value);
}
return ret;
},
/**
* Get option string by index.
*
* #param integer index
* #return string
*/
getOptionByIndex: function(value) {
ret = null;
if (this.options.options[value]) {
ret = this.options.options[value];
}
return ret;
},
disable: function disable() {
this.options.disabled = true;
this.$element.addClass('disabled');
this.$input.attr('disabled', '');
this.$element.find('button').addClass('disabled');
},
enable: function enable() {
this.options.disabled = false;
this.$element.removeClass('disabled');
this.$input.removeAttr('disabled');
this.$element.find('button').removeClass('disabled');
},
keydown: function keydown(event) {
var keyCode = event.keyCode;
if (keyCode === 38) {
this.step(true);
} else if (keyCode === 40) {
this.step(false);
} else if (keyCode === 13) {
this.change();
}
},
keyup: function keyup(event) {
var keyCode = event.keyCode;
if (keyCode === 38 || keyCode === 40) {
this.triggerChangedEvent();
}
},
bindMousewheelListeners: function bindMousewheelListeners() {
var inputEl = this.$input.get(0);
if (inputEl.addEventListener) {
//IE 9, Chrome, Safari, Opera
inputEl.addEventListener('mousewheel', $.proxy(this.mousewheelHandler, this), false);
// Firefox
inputEl.addEventListener('DOMMouseScroll', $.proxy(this.mousewheelHandler, this), false);
} else {
// IE <9
inputEl.attachEvent('onmousewheel', $.proxy(this.mousewheelHandler, this));
}
},
mousewheelHandler: function mousewheelHandler(event) {
if (!this.options.disabled) {
var e = window.event || event;// old IE support
var delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail)));
var self = this;
clearTimeout(this.mousewheelTimeout);
this.mousewheelTimeout = setTimeout(function () {
self.triggerChangedEvent();
}, 300);
if (delta > 0) {//ACE
this.step(true);
} else {
this.step(false);
}
if (e.preventDefault) {
e.preventDefault();
} else {
e.returnValue = false;
}
return false;
}
}
};
// SPINSTRINGS PLUGIN DEFINITION
$.fn.spinstrings = function spinstrings(option) {
var args = Array.prototype.slice.call(arguments, 1);
var methodReturn;
var $set = this.each(function () {
var $this = $(this);
var data = $this.data('fu.spinstrings');
var options = typeof option === 'object' && option;
if (!data) {
$this.data('fu.spinstrings', (data = new SpinStrings(this, options)));
}
if (typeof option === 'string') {
methodReturn = data[option].apply(data, args);
}
});
return (methodReturn === undefined) ? $set : methodReturn;
};
// value needs to be 0 for this.render();
$.fn.spinstrings.defaults = {
value: null,
index: 0,
min: 0,
max: 0,
step: 1,
disabled: false,
cycle: true
};
$.fn.spinstrings.Constructor = SpinStrings;
$.fn.spinstrings.noConflict = function noConflict() {
$.fn.spinstrings = old;
return this;
};
// DATA-API
$(document).on('mousedown.fu.spinstrings.data-api', '[data-initialize=spinstrings]', function (e) {
var $control = $(e.target).closest('.spinstrings');
if (!$control.data('fu.spinstrings')) {
$control.spinstrings($control.data());
}
});
// Must be domReady for AMD compatibility
$(function () {
$('[data-initialize=spinstrings]').each(function () {
var $this = $(this);
if (!$this.data('fu.spinstrings')) {
$this.spinstrings($this.data());
}
});
});
// -- BEGIN UMD WRAPPER AFTERWORD --
}));
// -- END UMD WRAPPER AFTERWORD --
Mark-up & style:
<style>
.spinstrings {position:relative;display:inline-block;margin-left:-2px}
.spinstrings input {height:26px;color:#444444;font-size:12px;padding:0 5px;margin:0 !important;width:100px}
.spinstrings input[readonly] {background-color:#ffffff !important}
.spinstrings .spinstrings-arrows {position:absolute;right:5px;height:22px;width:12px;background:transparent;top:1px}
.spinstrings .spinstrings-arrows i.fa {position:absolute;display:block;font-size:16px;line-height:10px;width:12px;cursor:pointer}
.spinstrings .spinstrings-arrows i.fa:hover {color:#307ecc}
.spinstrings .spinstrings-arrows i.fa.spinstrings-up {top:0}
.spinstrings .spinstrings-arrows i.fa.spinstrings-down {bottom:0}
</style>
<div class="spinstrings" id="mySpinStrings">
<input type="text" class="spinstrings-input" readonly="readonly">
<div class="spinstrings-arrows"><i class="fa fa-caret-up spinstrings-up"></i><i class="fa fa-caret-down spinstrings-down"></i></div>
</div>
Init as:
$('#mySpinStrings', expiryControlsW).spinstrings({
options: ['day', 'week', 'month'],
value: 'week'
});
That's it, folks!

Resources