c9-core/plugins/c9.ide.collab/timeslider/timeslider.js

784 wiersze
28 KiB
JavaScript

define(function(require, exports, module) {
"use strict";
main.consumes = [
"Plugin", "c9", "ui", "ace", "tabManager", "settings", "menus",
"commands", "save", "layout", "util"
];
main.provides = ["timeslider"];
return main;
function main(options, imports, register) {
var Plugin = imports.Plugin;
var c9 = imports.c9;
var ui = imports.ui;
var ace = imports.ace;
var util = imports.util;
var tabs = imports.tabManager;
var settings = imports.settings;
var menus = imports.menus;
var layout = imports.layout;
var commands = imports.commands;
var save = imports.save;
var html = require("text!./timeslider.html");
var css = require("text!./timeslider.css");
var dom = require("ace/lib/dom");
var isLoading;
var tsVisibleKey = "user/collab/@timeslider-visible";
// timeslider keyboard handler
var timesliderKeyboardHandler = {
handleKeyboard: function(data, hashId, keystring) {
if (keystring == "esc") {
forceHideSlider();
return { command: "null" };
}
}
};
// UI elements
var container, timeslider, timesliderClose, slider;
var sliderBar, handle, playButton, playButtonIcon, revisionInfo;
var revisionDate, revisionLabel, leftStep, rightStep, revertButton;
var sliderProgress, activeDocument;
/***** Initialization *****/
var plugin = new Plugin("Ajax.org", main.consumes);
var emit = plugin.getEmitter();
var sliderLength = 1000;
var sliderPos = 0;
var sliderActive = false;
var savedRevisions = [];
var savedRevisionNums = [];
var sliderPlaying = false;
// This number is calibrated from UI experimentation
var LEFT_PADDING = 64;
var loaded = false;
function load(callback) {
if (loaded) return false;
loaded = true;
commands.addCommand({
name: "toggleTimeslider",
exec: toggleTimeslider,
isAvailable: timesliderAvailable
}, plugin);
commands.addCommand({
name: "forceToggleTimeslider",
exec: function() {
var isVisible = settings.getBool(tsVisibleKey);
settings.set(tsVisibleKey, !isVisible);
toggleTimeslider();
},
isAvailable: timesliderAvailable
}, plugin);
menus.addItemByPath("File/Show File Revision History", new ui.item({
type: "check",
checked: "" + tsVisibleKey + "",
command: "toggleTimeslider"
}), 1240, plugin);
menus.addItemByPath("File/~", new ui.divider(), 1250, plugin);
settings.on("read", function () {
// force-hide-timeslider with initial loading
settings.set(tsVisibleKey, false);
}, plugin);
// right click context item in ace
var mnuCtxEditorFileHistory = new ui.item({
caption: "File History",
command: "forceToggleTimeslider"
}, plugin);
ace.getElement("menu", function(menu) {
menus.addItemToMenu(menu, mnuCtxEditorFileHistory, 500, plugin);
menus.addItemToMenu(menu, new ui.divider(), 550, plugin);
menu.on("prop.visible", function(e) {
// only fire when visibility is set to true
if (e.value) {
var editor = tabs.focussedTab.editor;
if (timesliderAvailable(editor))
mnuCtxEditorFileHistory.enable();
else
mnuCtxEditorFileHistory.disable();
}
});
});
tabs.on("paneDestroy", function (e) {
if (!tabs.getPanes(tabs.container).length)
forceHideSlider();
}, plugin);
tabs.on("focusSync", function(e) {
util.nextFrame(function() {
var doc = getTabCollabDocument(e.tab);
if (activeDocument && isVisible) {
if (activeDocument == doc) return;
hide();
isVisible = true;
}
activeDocument = doc;
if (!isVisible)
return;
if (!doc)
return forceHideSlider();
show();
doc.loadRevisions();
});
}, plugin);
save.on("beforeSave", function(e) {
if (isVisible)
return false;
}, plugin);
plugin.on("slider", function (revNum) {
if (!activeDocument || !isVisible)
return;
schedule(revNum);
});
}
var scheduled, lastRevNum;
function schedule(revNum) {
lastRevNum = revNum;
if (scheduled) return;
util.nextFrame(function() {
lastRevNum && activeDocument.updateToRevision(lastRevNum);
scheduled = false;
});
}
var drawn = false;
function draw () {
if (drawn) return;
drawn = true;
ui.insertHtml(null, html, plugin);
ui.insertCss(css, null, plugin);
function $(id) {
return document.getElementById(id);
}
var ext = $("timeslider-top");
timeslider = $("timeslider");
timesliderClose = $("timeslider_close");
slider = $("timeslider-slider");
sliderBar = $("ui-slider-bar");
sliderProgress = $("ui-slider-progress");
handle = $("ui-slider-handle");
playButton = $("playpause_button");
playButtonIcon = $("playpause_button_icon");
revisionInfo = $("revision_info");
revisionDate = $("revision_date");
revisionLabel = $("revision_label");
leftStep = $("leftstep");
rightStep = $("rightstep");
revertButton = $("revert_to_rev");
var tbcont = tabs.container;
var box = new ui.vsplitbox({});
tbcont.parentNode.insertBefore(box, tbcont.nextSibling);
container = box.appendChild(new ui.bar({ height: 64 }));
container.$ext.appendChild(ext);
box.appendChild(tbcont);
box.$ext.style.top = 0; // Works around an APF bug
timesliderClose.addEventListener("click", forceHideSlider);
disableSelection(playButton);
disableSelection(timeslider);
layout.on("resize", function() {
updateSliderElements();
}, plugin);
slider.addEventListener("mousedown", function(evt) {
if (evt.target.className == "star" && !sliderActive)
onBarMouseDown(evt);
});
sliderBar.addEventListener("mousedown", onBarMouseDown);
// Slider dragging
handle.addEventListener("mousedown", onHandleMouseDown);
// play/pause toggling
playButton.addEventListener("mousedown", function(evt) {
playButton.addEventListener("mouseup", function onMouseUp(evt2) {
playButton.removeEventListener("mouseup", onMouseUp);
playpause();
});
document.addEventListener("mouseup", function onMouseUp(evt2) {
document.removeEventListener("mouseup", onMouseUp);
});
});
revertButton.addEventListener("click", function() {
if (!isRevertAvailable())
return console.log("Revert not available");
console.log("Revert", activeDocument.id, "to rev", sliderPos);
hide();
activeDocument.revertToRevision(sliderPos);
});
// next/prev revisions and changeset
var steppers = [leftStep, rightStep];
steppers.forEach(function (stepper) {
stepper.addEventListener("mousedown", function(evt) {
var origcss = stepper.style["background-position"];
if (!origcss)
origcss = stepper.style["background-position-x"].split("px")[0] + " " + stepper.style["background-position-y"].split("px")[0];
var origpos = parseInt(origcss.split(" ")[1], 10);
var newpos = origpos - 43;
if (newpos < 0)
newpos += 87;
var newcss = (origcss.split(" ")[0] + " " + newpos + "px");
if (stepper.style.opacity != 1.0)
newcss = origcss;
stepper.style["background-position"] = newcss;
var pos, nextStar, i;
stepper.addEventListener("mouseup", function onMouseUp(evt2) {
stepper.style["background-position"] = origcss;
stepper.removeEventListener("mouseup", onMouseUp);
// document.removeEventListener("mouseup", onMouseUp);
});
document.addEventListener("mouseup", function onMouseUp(evt2) {
stepper.style["background-position"] = origcss;
document.removeEventListener("mouseup", onMouseUp);
// stepper.removeEventListener("mouseup", onMouseUp);
});
var id = stepper.id;
if (id == "leftstep") {
setSliderPosition(sliderPos - 1);
}
else if (id == "rightstep") {
setSliderPosition(sliderPos + 1);
}
else if (id == "leftstar") {
nextStar = 0; // default to first revision in document
for (i = 0; i < savedRevisionNums.length; i++) {
pos = savedRevisionNums[i];
if (pos < sliderPos && nextStar < pos)
nextStar = pos;
}
setSliderPosition(nextStar);
}
else if (id == "rightstar") {
nextStar = sliderLength; // default to last revision in document
for (i = 0; i < savedRevisionNums.length; i++) {
pos = savedRevisionNums[i];
if (pos > sliderPos && nextStar > pos)
nextStar = pos;
}
setSliderPosition(nextStar);
}
});
});
emit("draw");
}
/***** Methods *****/
function disableSelection(element) {
element.onselectstart = function() {
return false;
};
element.unselectable = "on";
element.style.MozUserSelect = "none";
element.style.cursor = "default";
}
function cumulativeOffset(element) {
var top = 0, left = 0;
do {
top += element.offsetTop || 0;
left += element.offsetLeft || 0;
element = element.offsetParent;
} while (element);
return {
top: top,
left: left
};
}
function setHandleLeft(pos) {
handle.style.left = pos + "px";
sliderProgress.style.width = (pos - sliderBar.offsetLeft + 10) + "px";
}
function onBarMouseDown(evt) {
var newloc = evt.clientX - cumulativeOffset(sliderBar).left;
var newSliderPos = Math.round(newloc * sliderLength / (sliderBar.offsetWidth - 2));
setHandleLeft(calcHandlerLeft(newSliderPos));
onHandleMouseDown(evt);
}
function onHandleMouseDown(evt) {
var startLoc = evt.clientX;
var currentLoc = parseInt(handle.style.left.split("px")[0], 10);
sliderActive = true;
function calcSliderPos(clientX) {
var newloc = currentLoc + (clientX - startLoc) - LEFT_PADDING;
if (newloc < 0)
newloc = 0;
var barWidth = sliderBar.offsetWidth - 2;
if (newloc > barWidth)
newloc = barWidth;
return Math.round(newloc * sliderLength / barWidth);
}
function clamp(clientX) {
var newloc = currentLoc + (clientX - startLoc);
var handleOverflow = (handle.offsetWidth / 2);
if (newloc < sliderBar.offsetLeft - handleOverflow + 1)
newloc = sliderBar.offsetLeft - handleOverflow + 1;
if (newloc > sliderBar.offsetLeft + sliderBar.offsetWidth - handleOverflow - 1)
newloc = sliderBar.offsetLeft + sliderBar.offsetWidth - handleOverflow - 1;
return newloc;
}
function onMouseMove(evt2) {
handle.style.pointer = "move";
handle.style.transition = "none";
sliderProgress.style.transition = "none";
var newSliderPos = calcSliderPos(evt2.clientX);
revisionLabel.textContent = "Version " + newSliderPos;
setHandleLeft(clamp(evt2.clientX));
if (sliderPos != newSliderPos) {
sliderPos = newSliderPos;
emit("slider", newSliderPos);
}
}
function onMouseUp(evt2) {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
sliderActive = false;
var newSliderPos = calcSliderPos(evt2.clientX);
currentLoc = calcHandlerLeft(newSliderPos);
setHandleLeft(currentLoc);
// if (sliderPos != Math.round(newloc * sliderLength / ($("#ui-slider-bar").width()-2)))
setSliderPosition(newSliderPos);
handle.style.transition = "left .1s";
sliderProgress.style.transition = "width .1s";
}
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
}
function isRevertAvailable() {
return !sliderPlaying && !sliderActive &&
!isLoading && activeDocument &&
sliderLength && sliderPos !== sliderLength;
}
var starWidth = 15;
function updateSliderElements() {
var prevX, prevStar, firstHidden;
for (var i = 0; i < savedRevisions.length; i++) {
var star = savedRevisions[i];
var position = parseInt(star.pos, 10);
var x = calcHandlerLeft(position) - 1;
if (x - prevX < 2 * starWidth) {
if (prevStar)
prevStar.style.opacity = 0.15;
prevStar = star;
if (!firstHidden) {
firstHidden = x;
} else if (x - firstHidden > 5 * starWidth) {
firstHidden = prevStar = null;
}
} else {
firstHidden = prevStar = null;
}
prevX = x;
star.style.left = x + "px";
star.style.opacity = 1;
}
setHandleLeft(calcHandlerLeft(sliderPos));
}
function addSavedRevision(revision) {
var position = revision.revNum;
var newSR = document.createElement("div");
newSR.className = "star";
newSR.title = "File Saved on " + dateFormat(revision.updated_at);
newSR.pos = position;
newSR.style.left = (calcHandlerLeft(position) - 1) + "px";
slider.appendChild(newSR);
newSR.addEventListener("mouseup", function() {
setSliderPosition(position);
});
savedRevisions.push(newSR);
savedRevisionNums.push(position);
if (position === sliderPos)
setSliderPosition(position);
}
function setSavedRevisions(revisions) {
toArray(slider.getElementsByClassName("star")).forEach(function (star) {
star.remove();
});
savedRevisions = [];
savedRevisionNums = [];
revisions.forEach(function(revision) {
addSavedRevision(revision);
});
}
function toArray(arg) {
return Array.prototype.slice.apply(arg);
}
function zpad(str, length) {
str = str + "";
while (str.length < length)
str = "0" + str;
return str;
}
function dateFormat(time) {
var date = new Date(time);
var month = ["Jan", "Feb", "March", "April", "May", "June", "July", "Aug", "Sept", "Oct", "Nov", "Dec"][date.getMonth()];
var day = zpad(date.getDate(), 2);
var year = date.getFullYear();
var hours = zpad(date.getHours(), 2);
var minutes = zpad(date.getMinutes(), 2);
var seconds = zpad(date.getSeconds(), 2);
return ([month, " ", day, ", ", year, " ", hours, ":", minutes, ":", seconds].join(""));
}
function updateTimer(time) {
revisionDate.textContent = dateFormat(time);
}
function calcHandlerLeft(pos) {
var left = pos * (sliderBar.offsetWidth - 2) / (sliderLength * 1.0);
left = (left || 0) + LEFT_PADDING;
return left;
}
function setSliderPosition(newpos) {
newpos = Number(newpos);
if (newpos < 0 || newpos > sliderLength) return;
setHandleLeft(calcHandlerLeft(newpos));
if (savedRevisionNums.indexOf(newpos) === -1) {
revisionLabel.textContent = "Version " + newpos;
revisionLabel.className = "revision_label";
}
else {
revisionLabel.textContent = "Saved Version " + newpos;
revisionLabel.className = "revision_label saved";
}
if (newpos === 0)
leftStep.className = "stepper disabled";
else
leftStep.className = "stepper";
if (newpos == sliderLength)
rightStep.className = "stepper disabled";
else
rightStep.className = "stepper";
sliderPos = newpos;
updateRevertVisibility();
emit("slider", newpos);
}
function updateRevertVisibility() {
if (isRevertAvailable())
revertButton.className = "revert";
else
revertButton.className = "revert disabled";
}
function getSliderLength() {
return sliderLength;
}
function setSliderLength(newlength) {
sliderLength = newlength;
updateSliderElements();
}
function playButtonUpdater() {
if (sliderPlaying) {
if (sliderPos + 1 > sliderLength) {
dom.toggleCssClass(playButtonIcon, "pause");
sliderPlaying = false;
return;
}
setSliderPosition(sliderPos + 1);
setTimeout(playButtonUpdater, 100);
}
}
function playpause() {
if (!isVisible)
return console.error("[Timeslider] Can't playpause while not visible");
dom.toggleCssClass(playButtonIcon, "pause");
if (!sliderPlaying) {
if (sliderPos == sliderLength) setSliderPosition(0);
sliderPlaying = true;
playButtonUpdater();
}
else {
sliderPlaying = false;
updateRevertVisibility();
}
}
function getCodeEditorTab() {
return $(".codeditorHolder .hsplitbox");
}
var isVisible = false;
var resizeInterval;
function useStoredState(e) {
if (e.state.filter && e.doc.meta.$storedState1)
e.state = e.doc.meta.$storedState1;
else if (!e.state.filter && e.doc.meta.$storedState0)
e.state = e.doc.meta.$storedState0;
}
function show() {
draw();
container.show();
clearInterval(resizeInterval);
var oldWidth = timeslider.offsetWidth;
resizeInterval = setInterval(function () {
if (timeslider.offsetWidth !== oldWidth) {
updateSliderElements();
oldWidth = timeslider.offsetWidth;
}
}, 100);
isVisible = true;
if (activeDocument) {
var tab = activeDocument.original.tab;
var aceEditor = tab.editor.ace;
aceEditor.setReadOnly(true);
aceEditor.keyBinding.addKeyboardHandler(timesliderKeyboardHandler);
aceEditor.renderer.onResize(true);
var doc = activeDocument.original;
doc.meta.$storedState0 = doc.getState();
doc.meta.$storedState1 = doc.getState(true);
doc.on("getState", useStoredState);
}
emit("visible", isVisible);
}
function hide() {
draw();
if (sliderPlaying)
playpause();
container.hide();
clearInterval(resizeInterval);
isVisible = false;
if (activeDocument) {
if (activeDocument.loaded)
activeDocument.updateToRevision();
var tab = activeDocument.original.tab;
var aceEditor = tab.editor.ace;
aceEditor.keyBinding.removeKeyboardHandler(timesliderKeyboardHandler);
aceEditor.setReadOnly(!!c9.readonly);
aceEditor.renderer.onResize(true);
var doc = activeDocument.original;
delete doc.meta.$storedState0;
delete doc.meta.$storedState1;
doc.off("getState", useStoredState);
}
emit("visible", isVisible);
}
function getTabCollabDocument(tab) {
var session = tab.path && tab.document.getSession();
if (!session) return false;
var aceSession = session.session;
return aceSession && aceSession.collabDoc;
}
function toggleTimeslider() {
if (isVisible) {
activeDocument && activeDocument.loaded && activeDocument.updateToRevision();
lastRevNum = null;
hide();
}
else {
// ide.dispatchEvent("track_action", {type: "timeslider"});
show();
lastRevNum = null;
activeDocument && activeDocument.loadRevisions();
}
}
function timesliderAvailable(editor) {
if (!editor || editor.type !== "ace")
return false;
var aceEditor = editor.ace;
var collabDoc = aceEditor.session.collabDoc;
return !!collabDoc;
}
function forceHideSlider() {
var tsVisible = isVisible || settings.getBool(tsVisibleKey);
if (tsVisible) {
settings.set(tsVisibleKey, false);
toggleTimeslider();
}
}
function setLoading(loading) {
if (loading === isLoading)
return;
isLoading = loading;
if (loading) {
// playButton.style["background-image"] = "url("+ staticPrefix + "/images/loading.gif)";
// playButton.style.margin = "7px 0px 0px 5px";
// playButtonIcon.style.display = revisionInfo.style.display = "none";
setSavedRevisions([]);
}
else {
// playButton.style["background-image"] = "url(" + staticPrefix + "/images/play_depressed.png)";
// playButton.style.margin = "";
// playButtonIcon.style.display = revisionInfo.style.display = "block";
}
}
/***** Lifecycle *****/
plugin.on("load", function() {
load();
});
plugin.on("enable", function() {
});
plugin.on("disable", function() {
});
plugin.on("unload", function() {
loaded = false;
drawn = false;
});
/***** Register and define API *****/
/**
* The timeslider allowing users to naigate through the file revision history, revert, step back and forth
* @singleton
*/
plugin.freezePublicAPI({
/**
* Specifies wether the timeslider is visible or not
* @property {Boolean} visible
*/
get visible() { return isVisible; },
/**
* Specifies wether the timeslider is in a loading state
* If true, then: the collab document is fetching the revisions from the collab server
* @property {Boolean} loading
*/
get loading() { return isLoading; },
/**
* Sets the loading state of the timeslider to match its active document's revisions fetching
* @property {Boolean} loading
*/
set loading(value) { setLoading(value); },
/**
* Gets the timeslider's active document or null, if not any
* @property {Document} activeDocument
*/
get activeDocument() { return activeDocument; },
/**
* Gets the timeslider's slider length
* @property {Number} sliderLength
*/
get sliderLength() { return getSliderLength(); },
/**
* Sets the timeslider's slider length
* @property {Number} sliderLength
*/
set sliderLength(len) { setSliderLength(len); },
/**
* Gets the timeslider's slider position
* @property {Number} sliderPosition
*/
get sliderPosition() { return sliderPos; },
/**
* Sets the timeslider's slider position
* @property {Number} sliderPosition
*/
set sliderPosition(pos) { setSliderPosition(pos); },
/**
* Update the revision timer element on the slider
* @param {Date} time
*/
updateTimer: updateTimer,
/**
* Update the saved revisions to be displayed as stars on the slider
* @param [{Revision}] revisions
*/
setSavedRevisions: setSavedRevisions,
/**
* Show the timeslider and load the current focussed tab's revisions into the UI
*/
show: show,
/**
* Hide the timeslider element and revert the activeDocument contents to the latest revision
*/
hide: hide,
/**
* Trigger the play or pause on the timeslider
*/
playpause: playpause,
/**
* Add a just-saved star revision to the timeslider
* @param {Revision} revision
*/
addSavedRevision: addSavedRevision
});
register(null, {
timeslider: plugin
});
}
});