add new timeline and find input to video player

This commit is contained in:
rolux 2011-05-16 13:29:26 +02:00
parent ac2ea5f53d
commit ce33d3650a
6 changed files with 326 additions and 162 deletions

View file

@ -27,14 +27,15 @@ Ox.load('UI', {
{'in': 5780.235, out: 5782.435}, {'in': 5780.235, out: 5782.435},
{'in': 5881.365, out: 5886.635} {'in': 5881.365, out: 5886.635}
], ],
timeline = 'http://next.0xdb.org/' + id + '/timeline.16.png', timeline = 'png/timeline.16.png',
url = 'http://next.0xdb.org/' + id + '/96p.webm', url = 'http://next.0xdb.org/' + id + '/96p.webm',
videoSize = getVideoSize(), videoSize = getVideoSize(),
$videos = [ $videos = [
Ox.VideoPlayer({ Ox.VideoPlayer({
controls: ['play', 'mute', 'fullscreen', 'scale', 'timeline', 'position'], controls: ['play', 'mute', 'fullscreen', 'scale', 'timeline', 'position'],
enableFind: true,
enableFullscreen: true,
enableKeyboard: true, enableKeyboard: true,
find: 'brick',
focus: 'mouseenter', focus: 'mouseenter',
height: 192, height: 192,
'in': 3128.725, 'in': 3128.725,
@ -47,7 +48,9 @@ Ox.load('UI', {
showIconOnLoad: true, showIconOnLoad: true,
showProgress: false, showProgress: false,
subtitles: 'srt/' + id + '.srt', subtitles: 'srt/' + id + '.srt',
timeline: timeline, timeline: function(i) {
return 'png/timeline.16.' + i + '.png';
},
title: '<b>Brick</b> - Rian Johnson - 2005', title: '<b>Brick</b> - Rian Johnson - 2005',
video: url + '?' + + Ox.random(1000000), video: url + '?' + + Ox.random(1000000),
width: 360 width: 360

BIN
demos/video/png/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -3,6 +3,7 @@ Ox.SmallVideoTimeline = function(options, self) {
self = self || {}; self = self || {};
var that = Ox.Element({}, self) var that = Ox.Element({}, self)
.defaults({ .defaults({
_offset: 0, // hack for cases where all these position: absolute elements have to go into a float: left
duration: 0, duration: 0,
editing: false, editing: false,
getTimelineURL: null, getTimelineURL: null,
@ -226,13 +227,15 @@ Ox.SmallVideoTimeline = function(options, self) {
self.$positionMarker.css({ self.$positionMarker.css({
left: self.interfaceLeft + Math.round( left: self.interfaceLeft + Math.round(
self.options.position * self.imageWidth / self.options.duration self.options.position * self.imageWidth / self.options.duration
) - (self.options.type == 'editor' ? 4 : 0) + 'px', ) - (self.options.type == 'editor' ? 4 : 0) + self.options._offset + 'px',
}); });
} }
function setWidth() { function setWidth() {
self.imageWidth = self.options.width - self.imageWidth = self.options.width -
(self.options.type == 'player' ? 16 : 8); (self.options.type == 'player' ? 16 : 8);
self.interfaceWidth = self.options.type == 'player' ?
self.options.width : self.imageWidth;
that.css({ that.css({
width: self.options.width + 'px' width: self.options.width + 'px'
}); });
@ -252,10 +255,24 @@ Ox.SmallVideoTimeline = function(options, self) {
} }
self.setOption = function(key, value) { self.setOption = function(key, value) {
if (key == 'paused') { if (key == 'duration') {
self.$image.options({
duration: value
});
} else if (key == 'paused') {
self.$positionMarkerRing.css({ self.$positionMarkerRing.css({
borderColor: 'rgba(255, 255, 255, ' + (self.options.paused ? 0.5 : 1) + ')' borderColor: 'rgba(255, 255, 255, ' + (self.options.paused ? 0.5 : 1) + ')'
}) })
} else if (key == 'position') {
setPositionMarker();
} else if (key == 'results') {
self.$image.options({
results: value
});
} else if (key == 'subtitles') {
self.$image.options({
subtitles: value
});
} else if (key == 'width') { } else if (key == 'width') {
setWidth(); setWidth();
} }

View file

@ -14,7 +14,6 @@ Ox.SmallVideoTimelineImage = function(options, self) {
type: 'player' type: 'player'
}) })
.options(options || {}) .options(options || {})
.addClass('OxSmallVideoTimeline')
.css({ .css({
position: 'absolute', position: 'absolute',
width: self.options.width + 'px' width: self.options.width + 'px'
@ -84,6 +83,8 @@ Ox.SmallVideoTimelineImage = function(options, self) {
.appendTo(that.$element); .appendTo(that.$element);
function getImageURL(image, callback) { function getImageURL(image, callback) {
Ox.print(image == 'results' || image == 'selection' ?
self.options.width : Math.ceil(self.options.duration))
var width = image == 'results' || image == 'selection' ? var width = image == 'results' || image == 'selection' ?
self.options.width : Math.ceil(self.options.duration), self.options.width : Math.ceil(self.options.duration),
height = self.imageHeight, height = self.imageHeight,
@ -160,21 +161,33 @@ Ox.SmallVideoTimelineImage = function(options, self) {
}); });
}); });
} else if (image == 'timeline') { } else if (image == 'timeline') {
var counter = 0, var $image, counter, images,
images = Math.ceil(self.options.duration / 3600),
top = self.options.type == 'player' ? 0 : 1; top = self.options.type == 'player' ? 0 : 1;
Ox.loop(images, function(i) { if (Ox.isString(self.options.timeline)) {
var $image = $('<img>') $image = $('<img>')
.attr({ .attr({
src: self.options.getTimelineURL(i) src: self.options.getTimelineURL(0)
}) })
.load(function() { .load(function() {
context.drawImage($image[0], i * 3600, top); context.drawImage($image[0])
if (++counter == images) { callback(canvas.toDataURL());
callback(canvas.toDataURL());
}
}); });
}) } else {
counter = 0;
images = Math.ceil(self.options.duration / 3600);
Ox.loop(images, function(i) {
var $image = $('<img>')
.attr({
src: self.options.getTimelineURL(i)
})
.load(function() {
context.drawImage($image[0], i * 3600, top);
if (++counter == images) {
callback(canvas.toDataURL());
}
});
});
}
} }
if (image != 'timeline') { if (image != 'timeline') {
context.putImageData(imageData, 0, 0); context.putImageData(imageData, 0, 0);

View file

@ -14,6 +14,8 @@ Ox.VideoPlayer <f> Generic Video Player
is just empty space that separates left-aligned from right-aligned is just empty space that separates left-aligned from right-aligned
controls controls
duration <n|-1> Duration (sec) duration <n|-1> Duration (sec)
enableFind <b|false> If true, enable find
enableFullscreen <b|false> If true, enable fullscreen
enableKeyboard <b|false> If true, enable keyboard controls enableKeyboard <b|false> If true, enable keyboard controls
externalControls <b|false> If true, controls are outside the video externalControls <b|false> If true, controls are outside the video
find <s|''> Query string find <s|''> Query string
@ -66,7 +68,9 @@ Ox.VideoPlayer = function(options, self) {
.defaults({ .defaults({
annotations: [], annotations: [],
controls: [], controls: [],
duration: 86399, duration: 0,
enableFind: false,
enableFullscreen: false,
enableKeyboard: false, enableKeyboard: false,
externalControls: false, externalControls: false,
find: '', find: '',
@ -135,6 +139,13 @@ Ox.VideoPlayer = function(options, self) {
key_1: function() { key_1: function() {
toggleScale(); toggleScale();
}, },
key_f: function() {
// need timeout so the "f" doesn't appear in the input field
setTimeout(self.$findInput.focusInput, 0);
},
key_g: function() {
goToNextResult(1);
},
key_left: function() { key_left: function() {
setPosition(self.options.position - self.secondsPerFrame, true); setPosition(self.options.position - self.secondsPerFrame, true);
}, },
@ -145,7 +156,10 @@ Ox.VideoPlayer = function(options, self) {
setPosition(self.options.position + self.secondsPerFrame, true); setPosition(self.options.position + self.secondsPerFrame, true);
}, },
key_shift_f: function() { key_shift_f: function() {
toggleFullscreen(true); self.options.enableFullscreen && toggleFullscreen(true);
},
key_shift_g: function() {
goToNextResult(-1);
}, },
key_space: function() { key_space: function() {
togglePaused(true); togglePaused(true);
@ -196,15 +210,24 @@ Ox.VideoPlayer = function(options, self) {
} else { } else {
Ox.get(self.options.subtitles, function(data) { Ox.get(self.options.subtitles, function(data) {
self.options.subtitles = Ox.parseSRT(data); self.options.subtitles = Ox.parseSRT(data);
self.results = find(self.options.find);
Ox.print('--setting results--', self.$timeline)
if (self.options.duration) {
self.$timeline && self.$timeline.options({
results: self.results,
subtitles: self.options.subtitles
});
} else {
}
}); });
//self.options.subtitles = []; self.options.subtitles = [];
} }
} }
self.results = find(self.options.find);
self.buffered = []; self.buffered = [];
self.controlsTimeout; self.controlsTimeout;
self.dragTimeout;
self.$videoContainer = $('<div>') self.$videoContainer = $('<div>')
.css({ .css({
@ -304,7 +327,7 @@ Ox.VideoPlayer = function(options, self) {
} }
} }
if (self.options.subtitles.length) { if (self.options.subtitles.length || true) { // fixme
self.$subtitle = $('<div>') self.$subtitle = $('<div>')
//.addClass('OxSubtitle') //.addClass('OxSubtitle')
.css({ .css({
@ -339,6 +362,114 @@ Ox.VideoPlayer = function(options, self) {
.appendTo(that.$element); .appendTo(that.$element);
} }
if (self.options.enableFind) {
self.$find = $('<div>')
.addClass('OxInterface')
.css({
position: 'absolute',
right: '16px',
top: (self.options.title ? 32 : 16) + 'px',
width: '128px',
borderRadius: '8px',
opacity: 0
})
.css({
backgroundImage: '-moz-linear-gradient(top, rgba(64, 64, 64, 0.5), rgba(0, 0, 0, 0.5))'
})
.css({
backgroundImage: '-webkit-linear-gradient(top, rgba(64, 64, 64, 0.5), rgba(0, 0, 0, 0.5))'
})
.appendTo(that.$element);
self.$previousButton = Ox.Button({
style: 'symbol',
title: 'arrowLeft',
tooltip: 'Previous [Shift+G]',
type: 'image'
})
.css({float: 'left', opacity: 0.25})
.bindEvent({
click: function() {
goToNextResult(-1);
}
})
.appendTo(self.$find);
self.$nextButton = Ox.Button({
style: 'symbol',
title: 'arrowRight',
tooltip: 'Next [G]',
type: 'image'
})
.css({float: 'left', opacity: 0.25})
.bindEvent({
click: function() {
goToNextResult(1);
}
})
.appendTo(self.$find);
self.$findInput = Ox.Input({
placeholder: 'Find',
value: self.options.find,
width: 86
})
.css({
float: 'left',
background: 'rgba(0, 0, 0, 0)',
MozBoxShadow: '0 0 0',
WebkitBoxShadow: '0 0 0'
})
.bindEvent({
focus: function() {
self.inputHasFocus = true;
},
blur: function() {
self.inputHasFocus = false;
submitFindInput();
}
})
.appendTo(self.$find);
self.$findInput.children('input').css({
width: (self.positionWidth - 6) + 'px',
height: '16px',
padding: '0 3px 0 3px',
border: '0px',
borderRadius: '8px',
fontSize: '11px',
color: 'rgb(255, 255, 255)'
})
.css({
background: '-moz-linear-gradient(top, rgba(0, 0, 0, 0.5), rgba(64, 64, 64, 0.5))'
})
.css({
background: '-webkit-linear-gradient(top, rgba(0, 0, 0, 0.5), rgba(64, 64, 64, 0.5))'
});
self.$clearButton = Ox.Button({
style: 'symbol',
title: 'close',
tooltip: 'Clear',
type: 'image'
})
.css({float: 'left'})
.bindEvent({
click: function() {
self.$findInput
.options({value: ''})
.focusInput();
self.results = [];
self.$timeline && self.$timeline.options({
results: self.results
});
}
})
.appendTo(self.$find);
}
if (self.options.controls.length) { if (self.options.controls.length) {
self.$controls = Ox.Bar({ self.$controls = Ox.Bar({
@ -463,39 +594,9 @@ Ox.VideoPlayer = function(options, self) {
.bindEvent('click', toggleSize) .bindEvent('click', toggleSize)
.appendTo(self.$controls); .appendTo(self.$controls);
}else if (control == 'timeline') { } else if (control == 'timeline') {
self.$timeline = Ox.Element() /*
.addClass('timeline')
.css({
float: 'left',
height: self.barHeight + 'px',
background: 'rgba(0, 0, 0, 0.75)',
borderRadius: self.barHeight / 2 + 'px'
})
.appendTo(self.$controls);
self.$timelineImages = Ox.Element()
.addClass('timelineimages')
.css({
float: 'left',
height: self.barHeight + 'px',
marginLeft: self.barHeight / 2 + 'px'
})
.appendTo(self.$timeline);
self.$timelineImage = $('<img>')
.addClass('timelineimage')
.attr({
src: self.options.timeline
})
.css({
float: 'left',
height: self.barHeight + 'px'
})
.appendTo(self.$timelineImages.$element);
///*
if (self.options.showProgress) { if (self.options.showProgress) {
self.$progress = $('<img>') self.$progress = $('<img>')
.attr({ .attr({
@ -507,46 +608,18 @@ Ox.VideoPlayer = function(options, self) {
}) })
.appendTo(self.$timelineImages.$element); .appendTo(self.$timelineImages.$element);
} }
//*/ */
self.$positionMarker = $('<div>') if (self.options.duration) {
.css({ self.$timeline = getTimeline()
float: 'left', } else {
width: '14px', self.$timeline = Ox.Element()
height: '14px', .css({
border: '1px solid rgba(0, 0, 0, 0.5)', float: 'left'
borderRadius: '8px' })
}) .html('&nbsp;');
.append( }
self.$positionMarkerRing = $('<div>') self.$timeline.appendTo(self.$controls);
.css({
width: '10px',
height: '10px',
border: '2px solid rgba(255, 255, 255, 0.5)',
borderRadius: '7px',
})
.append(
$('<div>')
.css({
width: '8px',
height: '8px',
border: '1px solid rgba(0, 0, 0, 0.5)',
borderRadius: '5px',
})
)
)
.appendTo(self.$timeline.$element);
self.$timelineInterface = Ox.Element()
.css({
float: 'left',
height: self.barHeight + 'px',
})
.appendTo(self.$controls);
self.$tooltip = Ox.Tooltip({
animate: false
});
} else if (control == 'space') { } else if (control == 'space') {
@ -659,6 +732,16 @@ Ox.VideoPlayer = function(options, self) {
} }
} }
function find(query) {
query = query.toLowerCase();
return query.length ? Ox.map(self.options.subtitles, function(subtitle) {
return subtitle.text.toLowerCase().indexOf(query) > -1 ? {
'in': subtitle['in'],
out: subtitle.out
} : null;
}) : [];
}
function formatPosition(position) { function formatPosition(position) {
position = Ox.isUndefined(position) ? self.options.position : position; position = Ox.isUndefined(position) ? self.options.position : position;
return Ox.formatDuration(position, self.options.showMilliseconds); return Ox.formatDuration(position, self.options.showMilliseconds);
@ -712,13 +795,6 @@ Ox.VideoPlayer = function(options, self) {
padding: playIconPadding + 'px', padding: playIconPadding + 'px',
borderRadius: Math.round(self.iconSize / 2) + 'px' borderRadius: Math.round(self.iconSize / 2) + 'px'
}; };
} else if (element == 'positionMarker') {
var position = self.options.duration ?
(self.options.position - self['in']) / self.options.duration : 0;
css = {
marginLeft: position * self.timelineImageWidth -
self.timelineImageWidth - 8 + 'px',
};
} else if (element == 'poster' || element == 'video') { } else if (element == 'poster' || element == 'video') {
var playerWidth = self.width, var playerWidth = self.width,
playerHeight = self.height, playerHeight = self.height,
@ -759,15 +835,6 @@ Ox.VideoPlayer = function(options, self) {
css = { css = {
width: self.timelineWidth + 'px' width: self.timelineWidth + 'px'
}; };
} else if (element == 'timelineImage' || element == 'timelineImages') {
css = {
width: self.timelineImageWidth + 'px'
};
} else if (element == 'timelineInterface') {
css = {
width: self.timelineWidth + 'px',
marginLeft: -self.timelineWidth + 'px'
};
} else if (element == 'videoContainer') { } else if (element == 'videoContainer') {
css = { css = {
width: self.width + 'px', width: self.width + 'px',
@ -841,6 +908,61 @@ Ox.VideoPlayer = function(options, self) {
return subtitle; return subtitle;
} }
function getTimeline() {
var $timeline = Ox.SmallVideoTimeline({
_offset: getTimelineLeft(),
duration: self.options.duration,
getTimelineURL: Ox.isString(self.options.timeline) ?
function() { return self.options.timeline; } :
self.options.timeline,
'in': self.options['in'],
out: self.options.out,
paused: self.options.paused,
results: self.results,
showMilliseconds: self.options.showMilliseconds,
subtitles: self.options.subtitles,
type: 'player',
width: getTimelineWidth()
})
.css({
float: 'left'
})
.bindEvent({
position: function(data) {
setPosition(data.position, true);
}
});
//Ox.print('??', $timeline.find('.OxInterface'))
$timeline.children().css({
marginLeft: getTimelineLeft() + 'px'
});
$timeline.find('.OxInterface').css({
marginLeft: getTimelineLeft() + 'px'
});
return $timeline;
}
function getTimelineLeft() {
var left = 0;
Ox.forEach(self.options.controls, function(control) {
if (control == 'timeline') {
return false;
}
left += control == 'position' ? self.positionWidth : 16
});
return left;
}
function getTimelineWidth() {
return (self.options.fullscreen ? window.innerWidth : self.options.width) -
self.options.controls.reduce(function(prev, curr) {
return prev + (
curr == 'timeline' || curr == 'space' ? 0 :
curr == 'position' ? self.positionWidth : 16
);
}, 0);
}
function hideInterface() { function hideInterface() {
Ox.print('hideInterface'); Ox.print('hideInterface');
self.interfaceTimeout = setTimeout(function() { self.interfaceTimeout = setTimeout(function() {
@ -874,6 +996,8 @@ Ox.VideoPlayer = function(options, self) {
function loadedmetadata() { function loadedmetadata() {
var hadDuration = !!self.options.duration;
self.loaded = true; self.loaded = true;
self.out = self.options.playInToOut && self.out = self.options.playInToOut &&
self.options.out < self.video.duration ? self.options.out < self.video.duration ?
@ -893,46 +1017,19 @@ Ox.VideoPlayer = function(options, self) {
} }
} }
if (self.$timeline) {
if (!hadDuration) {
self.$timeline.replaceWith(self.$timeline = getTimeline());
}
self.$timeline.options({
duration: self.options.duration
});
}
if (self.options.enableKeyboard && self.options.focus == 'load') { if (self.options.enableKeyboard && self.options.focus == 'load') {
that.gainFocus(); that.gainFocus();
} }
self.$timeline && self.$timelineInterface
.bind({
mousedown: mousedownTrack,
mouseleave: mouseleaveTrack,
mousemove: mousemoveTrack,
})
.bindEvent({
drag: dragTrack,
dragpause: dragpauseTrack,
dragend: dragpauseTrack
});
}
function dragTrack(e) {
setPosition(getPosition(e), true);
if (self.dragTimeout) {
clearTimeout(self.dragTimeout);
self.dragTimeout = 0;
}
}
function dragpauseTrack(e) {
self.video.currentTime = self.options.position;
}
function mousedownTrack(e) {
setPosition(getPosition(e), true);
}
function mouseleaveTrack(e) {
self.$tooltip.hide();
}
function mousemoveTrack(e) {
self.$tooltip.options({
title: formatPosition(getPosition(e))
}).show(e);
} }
function parsePositionInput(str) { function parsePositionInput(str) {
@ -1019,7 +1116,9 @@ Ox.VideoPlayer = function(options, self) {
self.posterIsVisible = false; self.posterIsVisible = false;
} }
self.$subtitle && setSubtitle(); self.$subtitle && setSubtitle();
self.$timeline && self.$positionMarker.css(getCSS('positionMarker')); self.$timeline && self.$timeline.options({
position: self.options.position
});
self.$position && self.$position.html(formatPosition()); self.$position && self.$position.html(formatPosition());
} }
@ -1031,12 +1130,13 @@ Ox.VideoPlayer = function(options, self) {
self.iconLeft = parseInt((self.width - self.iconSize) / 2), self.iconLeft = parseInt((self.width - self.iconSize) / 2),
self.iconTop = parseInt((self.height - self.iconSize) / 2); self.iconTop = parseInt((self.height - self.iconSize) / 2);
if (self.$timeline || self.$space) { if (self.$timeline || self.$space) {
self.timelineWidth = self.width - self.options.controls.reduce(function(prev, curr) { self.timelineWidth = self.width -
return prev + ( self.options.controls.reduce(function(prev, curr) {
curr == 'timeline' || curr == 'space' ? 0 : return prev + (
curr == 'position' ? self.positionWidth : 16 curr == 'timeline' || curr == 'space' ? 0 :
); curr == 'position' ? self.positionWidth : 16
}, 0); );
}, 0);
if (self.$timeline) { if (self.$timeline) {
self.timelineImageWidth = self.timelineWidth - self.barHeight; self.timelineImageWidth = self.timelineWidth - self.barHeight;
} }
@ -1052,12 +1152,11 @@ Ox.VideoPlayer = function(options, self) {
self.$title && self.$title.animate(getCSS('title'), ms); self.$title && self.$title.animate(getCSS('title'), ms);
self.$controls && self.$controls.animate(getCSS('controls'), ms); self.$controls && self.$controls.animate(getCSS('controls'), ms);
if (self.$timeline) { if (self.$timeline) {
self.$timeline.animate(getCSS('timeline'), ms); self.$timeline.animate(getCSS('timeline'), ms, function() {
self.$timelineImages.animate(getCSS('timelineImages'), ms); self.$timeline.options({
self.$timelineImage.animate(getCSS('timelineImage'), ms); width: self.timelineWidth
self.$progress && self.$progress.animate(getCSS('progress'), ms); })
self.$positionMarker.animate(getCSS('positionMarker'), ms); });
self.$timelineInterface.animate(getCSS('timelineInterface'), ms);
} }
self.$space && self.$space.animate(getCSS('space'), ms); self.$space && self.$space.animate(getCSS('space'), ms);
} }
@ -1115,6 +1214,37 @@ Ox.VideoPlayer = function(options, self) {
}).show(); }).show();
} }
function submitFindInput() {
self.options.find = self.$findInput.options('value');
self.results = find(self.options.find);
self.$timeline && self.$timeline.options({
results: self.results
});
if (self.results.length) {
setPosition(self.results[0]['in'] + self.secondsPerFrame, true);
self.currentResult = 0;
}
}
function goToNextResult(direction) {
var found = false,
position = 0;
direction == -1 && self.results.reverse();
Ox.forEach(self.results, function(v) {
if (direction == 1 ? v['in'] > self.options.position : v['out'] < self.options.position) {
position = v['in'];
found = true;
return false;
}
});
direction == -1 && self.results.reverse();
if (!found) {
position = self.results[direction == 1 ? 0 : self.results.length - 1]['in'];
}
Ox.print('>>', self.results, position)
setPosition(position + self.secondsPerFrame, true);
}
function submitPositionInput() { function submitPositionInput() {
self.$positionInput.hide(); self.$positionInput.hide();
self.$position.html('').show(); self.$position.html('').show();
@ -1211,8 +1341,8 @@ Ox.VideoPlayer = function(options, self) {
function togglePaused(toggleButton) { function togglePaused(toggleButton) {
self.options.paused = !self.options.paused; self.options.paused = !self.options.paused;
self.$timeline && self.$positionMarkerRing.css({ self.$timeline && self.$timeline.options({
borderColor: 'rgba(255, 255, 255, ' + (self.options.paused ? 0.5 : 1) + ')' paused: self.options.paused
}); });
if (self.options.paused) { if (self.options.paused) {
self.video.pause(); self.video.pause();

View file

@ -3864,11 +3864,12 @@ Ox.highlight <f> Highlight matches in a string
> Ox.highlight('foobar', 'foo', 'match') > Ox.highlight('foobar', 'foo', 'match')
'<span class="match">foo</span>bar' '<span class="match">foo</span>bar'
@*/ @*/
// fixme: with regexp, special chars would have to be escaped
Ox.highlight = function(txt, str, classname) { Ox.highlight = function(txt, str, classname) {
return txt.replace( return str.length ? txt.replace(
new RegExp('(' + str + ')', 'ig'), new RegExp('(' + str + ')', 'ig'),
'<span class="' + classname + '">$1</span>' '<span class="' + classname + '">$1</span>'
); ) : txt;
}; };
/*@ /*@