Update to current webodf: saves on closing the session/document (needs server support still)

saves only is the user has edited something
pull/1/head
Friedrich W. H. Kossebau 11 years ago
parent abdd34ca50
commit e74535bdf6

@ -33,9 +33,7 @@ var documentsMain = {
return;
}
OC.addScript('documents', 'editor/boot_editor').done(function() {
var doclocation = response.es_id;
require({ }, ["webodf/editor/server/owncloud/ServerFactory", "webodf/editor/Editor"], function (ServerFactory, Editor) {
// fade out file list and show WebODF canvas
$('.documentslist, #emptyfolder').fadeOut('slow').promise().done(function() {
// odf action toolbar
@ -67,6 +65,9 @@ var documentsMain = {
' </div>' +
' </div>' +
'</div>';
var serverFactory = new ServerFactory();
$(document.body).addClass("claro");
$('.documentslist, #emptyfolder').after(canvashtml);
// in case we are on the public sharing page we shall display the odf into the preview tag
@ -74,21 +75,13 @@ var documentsMain = {
runtime.assert(response.es_id, "invalid session id.");
memberId = response.member_id;
webodfEditor.boot(
{
collaborative: "owncloud",
docUrl: doclocation,
loginProcedure: function(cb) {
cb(response.es_id, OC.currentUser, "token");
},
joinSession: function(userId, sessionId, cb) {
cb(memberId);
},
callback: function(webodfEditorInstance) {
documentsMain.webodfEditorInstance = webodfEditorInstance;
}
}
);
documentsMain.webodfServerInstance = serverFactory.createServer();
documentsMain.webodfEditorInstance = new Editor({}, documentsMain.webodfServerInstance, serverFactory);
// load the document and get called back when it's live
documentsMain.webodfEditorInstance.loadSession(response.es_id, memberId, function() {
documentsMain.webodfEditorInstance.startEditing();
});
});
});
},
@ -155,24 +148,24 @@ var documentsMain = {
onClose: function() {
"use strict";
var saveSessionRoute = OC.Router.generate('documents_session_save');
//auto save document
documentsMain.webodfEditorInstance.saveDocument(saveSessionRoute, function(){});
//close editor
documentsMain.webodfEditorInstance.shutdown(function() {
documentsMain.webodfEditorInstance.endEditing();
documentsMain.webodfEditorInstance.closeDocument(function() {
// successfull shutdown - all is good.
// TODO: proper session leaving call to server, either by webodfServerInstance or custom
// documentsMain.webodfServerInstance.leaveSession(sessionId, memberId, function() {
// Fade out odf-toolbar
$('#odf-toolbar').fadeOut('slow');
// Fade out editor
$('#mainContainer').fadeOut('slow', function() {
$('#mainContainer').remove();
$('#odf-canvas').remove();
$('.actions,#file_access_panel').fadeIn('slow');
$('.documentslist, #emptyfolder').fadeIn('slow');
$(document.body).removeClass('claro');
});
// Fade out odf-toolbar
$('#odf-toolbar').fadeOut('slow');
// Fade out editor
$('#mainContainer').fadeOut('slow', function() {
$('#mainContainer').remove();
$('#odf-canvas').remove();
$('.actions,#file_access_panel').fadeIn('slow');
$('.documentslist, #emptyfolder').fadeIn('slow');
$(document.body).removeClass('claro');
});
// });
});
},
loadDocuments: function () {

@ -36,28 +36,27 @@
define("webodf/editor/Editor", [
"dojo/i18n!webodf/editor/nls/myResources",
"webodf/editor/EditorSession",
"webodf/editor/MemberList",
"webodf/editor/MemberListView",
"dijit/layout/BorderContainer",
"dijit/layout/ContentPane",
"webodf/editor/widgets"],
function (myResources,
EditorSession,
MemberList,
MemberListView,
BorderContainer,
ContentPane,
loadWidgets) {
ToolBarTools) {
"use strict";
runtime.loadClass('odf.OdfCanvas');
/**
* @constructor
* @param {{networked:boolean=,
* memberid:!string,
* @param {{unstableFeaturesEnabled:boolean=,
* loadCallback:function()=,
* saveCallback:function()=,
* cursorAddedCallback:function(!string)=,
* cursorRemovedCallback:function(!string)=,
* registerCallbackForShutdown:function(!function())= }} args
* closeCallback:function()=,
* @param {!ops.Server=} server
* @param {!ServerFactory=} serverFactory
*/
@ -65,26 +64,25 @@ define("webodf/editor/Editor", [
var self = this,
// Private
memberid = args.memberid,
session,
editorSession,
memberList,
networked = args.networked === true,
opRouter,
memberModel,
memberListView,
toolbarTools,
loadOdtFile = args.loadCallback,
saveOdtFile = args.saveCallback,
cursorAddedHandler = args.cursorAddedCallback,
cursorRemovedHandler = args.cursorRemovedCallback,
registerCallbackForShutdown = args.registerCallbackForShutdown,
documentUrl,
odfCanvas;
function translator(key, context) {
if (undefined === myResources[key]) {
return "translation missing: " + key;
close = args.closeCallback,
odfCanvas,
pendingMemberId,
pendingEditorReadyCallback;
function getFileBlob(cbSuccess, cbError) {
var odfContainer = odfCanvas.odfContainer();
if (odfContainer) {
odfContainer.createByteArray(cbSuccess, cbError);
} else {
cbError("No odfContainer!");
}
return myResources[key];
}
/**
@ -94,37 +92,188 @@ define("webodf/editor/Editor", [
* which will insert the the cursor.
*
* @param {!string} initialDocumentUrl
* @param {!string} memberId
* @param {!function()} editorReadyCallback
* @return {undefined}
*/
function initGuiAndDoc(initialDocumentUrl, editorReadyCallback) {
var odfElement, mainContainer,
editorPane, memberListPane,
inviteButton,
viewOptions = {
editInfoMarkersInitiallyVisible: networked,
caretAvatarsInitiallyVisible: networked,
caretBlinksOnRangeSelect: true
},
memberListDiv = document.getElementById('memberList');
if (networked) {
runtime.assert(memberListDiv, "missing memberList div in HTML");
function initDocLoading(initialDocumentUrl, memberId, editorReadyCallback) {
runtime.assert(initialDocumentUrl, "document should be defined here.");
runtime.assert(memberId !== undefined, "memberId should be defined here.");
runtime.assert(!pendingEditorReadyCallback, "pendingEditorReadyCallback should not exist here.");
runtime.assert(!editorSession, "editorSession should not exist here.");
runtime.assert(!session, "session should not exist here.");
pendingMemberId = memberId;
pendingEditorReadyCallback = editorReadyCallback;
odfCanvas.load(initialDocumentUrl);
odfCanvas.setEditable(false);
}
/**
* create the editor, load the starting document,
* call editorReadyCallback once everything is done.
*
* @param {!string} docUrl
* @param {!string} memberId
* @param {!function()} editorReadyCallback
* @return {undefined}
*/
this.loadDocument = function (docUrl, memberId, editorReadyCallback) {
initDocLoading(docUrl, memberId, editorReadyCallback);
};
/**
* @param {!string} filename
* @param {?function()} callback
* @return {undefined}
*/
this.saveDocument = function (filename, callback) {
function onsuccess(data) {
var mimebase = "application/vnd.oasis.opendocument.",
mimetype = mimebase + "text",
blob;
filename = filename || "doc.odt";
if (filename.substr(-4) === ".odp") {
mimetype = mimebase + "presentation";
} else if (filename.substr(-4) === ".ods") {
mimetype = mimebase + "spreadsheet";
}
blob = new Blob([data.buffer], {type: mimetype});
saveAs(blob, filename);
}
function onerror(error) {
alert(error);
}
getFileBlob(onsuccess, onerror);
};
/**
* create the editor, load the starting document of an
* editing-session, request a replay of previous operations, call
* editorReadyCallback once everything is done.
*
* @param {!string} sessionId
* @param {!string} memberId
* @param {!function()} editorReadyCallback
* @return {undefined}
*/
this.loadSession = function (sessionId, memberId, editorReadyCallback) {
initDocLoading(server.getGenesisUrl(sessionId), memberId, function () {
var opRouter, memberModel;
// overwrite router and member model
// TODO: serverFactory should be a backendFactory,
// and there should be a backendFactory for local editing
opRouter = serverFactory.createOperationRouter(sessionId, memberId, server, odfCanvas.odfContainer());
session.setOperationRouter(opRouter);
memberModel = serverFactory.createMemberModel(sessionId, server);
session.setMemberModel(memberModel);
opRouter.requestReplay(function done() {
editorReadyCallback();
});
});
};
/**
* Closes the current editing running editing (polling-timer),
* cleanup.
* @param {!function(!Object=)} callback, passing an error object in case of error
* @return {undefined}
*/
this.closeDocument = function (callback) {
runtime.assert(session, "session should exist here.");
if (memberListView) {
memberListView.setEditorSession(undefined);
}
// TODO: there is a better pattern for this instead of unrolling
session.getOperationRouter().close(function(err) {
if (err) {
callback(err);
} else {
session.getMemberModel().close(function(err) {
if (err) {
callback(err);
} else {
editorSession.close(function(err) {
if (err) {
callback(err);
} else {
editorSession = undefined;
session.close(function(err) {
if (err) {
callback(err);
} else {
session = undefined;
callback();
}
});
}
});
}
});
}
});
};
runtime.loadClass('odf.OdfCanvas');
/**
* Adds a cursor and enables the tools and allows modifications.
* Should be called inside/after editorReadyCallback.
* TODO: turn this and endEditing() into readonly switch
* @return {undefined}
*/
this.startEditing = function () {
runtime.assert(editorSession, "editorSession should exist here.");
// we might need it later
documentUrl = initialDocumentUrl;
runtime.assert(documentUrl, "document should be defined here.");
toolbarTools.setEditorSession(editorSession);
editorSession.sessionController.startEditing();
};
runtime.assert(memberid !== undefined, "memberid should be defined here.");
/**
* Removes the cursor and disables the tools and allows modifications.
* Should be called before closeDocument, if startEditing was called before
* @return {undefined}
*/
this.endEditing = function () {
runtime.assert(editorSession, "editorSession should exist here.");
toolbarTools.setEditorSession(undefined);
editorSession.sessionController.endEditing();
};
odfElement = document.getElementById("canvas");
runtime.assert(odfElement, "initGuiAndDoc failed to get odf canvas from html");
odfCanvas = new odf.OdfCanvas(odfElement);
// make the canvas accessible to users of editor.js
self.odfCanvas = odfCanvas;
// init
function init() {
var mainContainer,
editorPane, memberListPane,
inviteButton,
canvasElement = document.getElementById("canvas"),
memberListElement = document.getElementById('memberList'),
collabEditing = Boolean(server),
directStylingEnabled = (! collabEditing) || args.unstableFeaturesEnabled,
// annotations not yet properly supported for OT
annotationsEnabled = (! collabEditing) || args.unstableFeaturesEnabled,
// undo manager is not yet integrated with collaboration
undoRedoEnabled = (! collabEditing),
closeCallback;
if (collabEditing) {
runtime.assert(memberListElement, 'missing "memberList" div in HTML');
}
runtime.assert(canvasElement, 'missing "canvas" div in HTML');
// setup translations
// TODO: move from document instance into webodf namespace
function translator(key, context) {
if (undefined === myResources[key]) {
return "translation missing: " + key;
}
return myResources[key];
}
document.translator = translator;
function translateContent(node) {
@ -147,43 +296,6 @@ define("webodf/editor/Editor", [
}
document.translateContent = translateContent;
odfCanvas.addListener("statereadychange", function () {
if (!editorReadyCallback) {
// already called once, restart session and return
// undo manager is not yet integrated with collaboration
if (! server) {
editorSession.sessionController.setUndoManager(new gui.TrivialUndoManager());
}
editorSession.startEditing();
return;
}
// Allow annotations
odfCanvas.enableAnnotations(true);
session = new ops.Session(odfCanvas);
editorSession = new EditorSession(session, memberid, {
viewOptions: viewOptions
});
// undo manager is not yet integrated with collaboration
if (! server) {
editorSession.sessionController.setUndoManager(new gui.TrivialUndoManager());
}
if (memberListDiv) {
memberList = new MemberList(editorSession, memberListDiv);
}
if (registerCallbackForShutdown) {
registerCallbackForShutdown(editorSession.endEditing);
}
loadWidgets(editorSession, loadOdtFile, saveOdtFile);
editorReadyCallback();
editorReadyCallback = null;
});
odfCanvas.load(initialDocumentUrl);
odfCanvas.setEditable(false);
// App Widgets
mainContainer = new BorderContainer({}, 'mainContainer');
@ -192,12 +304,13 @@ define("webodf/editor/Editor", [
}, 'editor');
mainContainer.addChild(editorPane);
if (networked && memberListDiv) {
if (collabEditing) {
memberListPane = new ContentPane({
region: 'right',
title: translator("members")
}, 'members');
mainContainer.addChild(memberListPane);
memberListView = new MemberListView(memberListElement);
}
mainContainer.startup();
@ -210,107 +323,48 @@ define("webodf/editor/Editor", [
inviteButton.onclick = window.inviteButtonProxy.clicked;
}
}
}
/**
* create the editor, load the starting document,
* call editorReadyCallback once everything is done.
*
* @param {!string} docUrl
* @param {!function()} editorReadyCallback
* @return {undefined}
*/
self.initAndLoadDocument = function (docUrl, editorReadyCallback) {
initGuiAndDoc(docUrl, function () {
editorReadyCallback(editorSession);
});
};
toolbarTools = new ToolBarTools({
loadOdtFile: loadOdtFile,
saveOdtFile: saveOdtFile,
close: close,
directStylingEnabled: directStylingEnabled,
annotationsEnabled: annotationsEnabled,
undoRedoEnabled: undoRedoEnabled
});
/**
* Shutdown running editing (polling-timer),
* cleanup.
*/
self.shutdown = function (successfullShutdownCallback) {
editorSession.endEditing();
opRouter.shutdown(function() {
memberModel.shutdown();
successfullShutdownCallback();
});
};
odfCanvas = new odf.OdfCanvas(canvasElement);
odfCanvas.enableAnnotations(annotationsEnabled);
/**
* Load a document in an editor that has already been initialized.
*/
self.loadDocument = function (docUrl) {
self.shutdown();
odfCanvas.load(docUrl);
};
odfCanvas.addListener("statereadychange", function () {
var viewOptions = {
editInfoMarkersInitiallyVisible: collabEditing,
caretAvatarsInitiallyVisible: collabEditing,
caretBlinksOnRangeSelect: true
};
/**
* @param {?function()} callback
* @return {undefined}
*/
self.saveDocument = function (filename, callback) {
function onsuccess(data) {
var mimebase = "application/vnd.oasis.opendocument.",
mimetype = mimebase + "text",
blob;
filename = filename || "doc.odt";
if (filename.substr(-4) === ".odp") {
mimetype = mimebase + "presentation";
} else if (filename.substr(-4) === ".ods") {
mimetype = mimebase + "spreadsheet";
// create session around loaded document
session = new ops.Session(odfCanvas);
editorSession = new EditorSession(session, pendingMemberId, {
viewOptions: viewOptions
});
if (undoRedoEnabled) {
editorSession.sessionController.setUndoManager(new gui.TrivialUndoManager());
}
blob = new Blob([data.buffer], {type: mimetype});
saveAs(blob, filename);
}
function onerror(error) {
alert(error);
}
var doc = odfCanvas.odfContainer();
doc.createByteArray(onsuccess, onerror);
};
/**
* create the editor, load the starting document of an
* editing-session, request a replay of previous operations, call
* editorReadyCallback once everything is done.
*
* @param {!string} sessionId
* @param {?function()} editorReadyCallback
*/
self.loadSession = function (sessionId, editorReadyCallback) {
initGuiAndDoc(server.getGenesisUrl(sessionId), function () {
// get router and member model
opRouter = opRouter || serverFactory.createOperationRouter(sessionId, memberid, server);
session.setOperationRouter(opRouter);
memberModel = memberModel || serverFactory.createMemberModel(sessionId, server);
session.setMemberModel(memberModel);
opRouter.requestReplay(function done() {
var odtDocument = session.getOdtDocument();
if (cursorAddedHandler) {
odtDocument.subscribe(ops.OdtDocument.signalCursorAdded, function (cursor) {
cursorAddedHandler(cursor.getMemberId());
});
}
if (cursorRemovedHandler) {
odtDocument.subscribe(ops.OdtDocument.signalCursorRemoved, function (memberId) {
cursorRemovedHandler(memberId);
});
}
editorReadyCallback(editorSession);
});
if (memberListView) {
memberListView.setEditorSession(editorSession);
}
// and report back to caller
pendingEditorReadyCallback();
// reset
pendingEditorReadyCallback = null;
pendingMemberId = null;
});
};
}
// access to member model
self.getMemberModel = function () {
return memberModel;
};
init();
}
return Editor;
});

@ -56,11 +56,11 @@ define("webodf/editor/EditorSession", [
/**
* Instantiate a new editor session attached to an existing operation session
* @param {!ops.Session} session
* @param {!string} memberid
* @param {!string} localMemberId
* @param {{viewOptions:gui.SessionViewOptions}} config
* @constructor
*/
var EditorSession = function EditorSession(session, memberid, config) {
var EditorSession = function EditorSession(session, localMemberId, config) {
var self = this,
currentParagraphNode = null,
currentNamedStyleName = null,
@ -80,7 +80,7 @@ define("webodf/editor/EditorSession", [
EditorSession.signalUndoStackChanged]);
this.sessionController = new gui.SessionController(session, memberid);
this.sessionController = new gui.SessionController(session, localMemberId);
this.sessionView = new gui.SessionView(config.viewOptions, session, new gui.CaretManager(self.sessionController));
this.availableFonts = [];
@ -160,11 +160,11 @@ define("webodf/editor/EditorSession", [
function uniqueParagraphStyleNCName(name) {
var result,
i = 0,
ncMemberId = createNCName(memberid),
ncMemberId = createNCName(localMemberId),
ncName = createNCName(name);
// create default paragraph style
// memberid is used to avoid id conflicts with ids created by other members
// localMemberId is used to avoid id conflicts with ids created by other members
result = ncName + "_" + ncMemberId;
// then loop until result is really unique
while (formatting.hasParagraphStyle(result)) {
@ -206,7 +206,7 @@ define("webodf/editor/EditorSession", [
odtDocument.subscribe(ops.OdtDocument.signalCursorMoved, function (cursor) {
// Emit 'cursorMoved' only when *I* am moving the cursor, not the other users
if (cursor.getMemberId() === memberid) {
if (cursor.getMemberId() === localMemberId) {
self.emit(EditorSession.signalCursorMoved, cursor);
}
});
@ -225,14 +225,6 @@ define("webodf/editor/EditorSession", [
odtDocument.subscribe(ops.OdtDocument.signalParagraphChanged, trackCurrentParagraph);
this.startEditing = function () {
self.sessionController.startEditing();
};
this.endEditing = function () {
self.sessionController.endEditing();
};
/**
* Call all subscribers for the given event with the specified argument
* @param {!string} eventid
@ -251,6 +243,15 @@ define("webodf/editor/EditorSession", [
eventNotifier.subscribe(eventid, cb);
};
/**
* @param {!string} eventid
* @param {!Function} cb
* @return {undefined}
*/
this.unsubscribe = function (eventid, cb) {
eventNotifier.unsubscribe(eventid, cb);
};
this.getMemberDetailsAndUpdates = function (memberId, subscriber) {
return session.getMemberModel().getMemberDetailsAndUpdates(memberId, subscriber);
};
@ -260,11 +261,11 @@ define("webodf/editor/EditorSession", [
};
this.getCursorPosition = function () {
return odtDocument.getCursorPosition(memberid);
return odtDocument.getCursorPosition(localMemberId);
};
this.getCursorSelection = function () {
return odtDocument.getCursorSelection(memberid);
return odtDocument.getCursorSelection(localMemberId);
};
this.getOdfCanvas = function () {
@ -280,7 +281,7 @@ define("webodf/editor/EditorSession", [
};
this.isBold = function () {
var cursor = odtDocument.getCursor(memberid);
var cursor = odtDocument.getCursor(localMemberId);
// no own cursor yet/currently added?
if (!cursor) {
return false;
@ -289,7 +290,7 @@ define("webodf/editor/EditorSession", [
};
this.isItalic = function () {
var cursor = odtDocument.getCursor(memberid);
var cursor = odtDocument.getCursor(localMemberId);
// no own cursor yet/currently added?
if (!cursor) {
return false;
@ -298,7 +299,7 @@ define("webodf/editor/EditorSession", [
};
this.hasUnderline = function () {
var cursor = odtDocument.getCursor(memberid);
var cursor = odtDocument.getCursor(localMemberId);
// no own cursor yet/currently added?
if (!cursor) {
return false;
@ -307,7 +308,7 @@ define("webodf/editor/EditorSession", [
};
this.hasStrikeThrough = function () {
var cursor = odtDocument.getCursor(memberid);
var cursor = odtDocument.getCursor(localMemberId);
// no own cursor yet/currently added?
if (!cursor) {
return false;
@ -323,7 +324,7 @@ define("webodf/editor/EditorSession", [
var op = new ops.OpApplyDirectStyling(),
selection = self.getCursorSelection();
op.init({
memberid: memberid,
memberId: localMemberId,
position: selection.position,
length: selection.length,
setProperties: value
@ -345,10 +346,10 @@ define("webodf/editor/EditorSession", [
length = Math.abs(length);
op.init({
memberid: memberid,
memberId: localMemberId,
position: position,
length: length,
name: memberid + Date.now()
name: localMemberId + Date.now()
});
session.enqueue(op);
};
@ -358,7 +359,7 @@ define("webodf/editor/EditorSession", [
if (currentNamedStyleName !== value) {
op = new ops.OpSetParagraphStyle();
op.init({
memberid: memberid,
memberId: localMemberId,
position: self.getCursorPosition(),
styleName: value
});
@ -369,7 +370,7 @@ define("webodf/editor/EditorSession", [
this.insertTable = function (initialRows, initialColumns, tableStyleName, tableColumnStyleName, tableCellStyleMatrix) {
var op = new ops.OpInsertTable();
op.init({
memberid: memberid,
memberId: localMemberId,
position: self.getCursorPosition(),
initialRows: initialRows,
initialColumns: initialColumns,
@ -409,7 +410,7 @@ define("webodf/editor/EditorSession", [
var op;
op = new ops.OpUpdateParagraphStyle();
op.init({
memberid: memberid,
memberId: localMemberId,
styleName: styleName,
setProperties: setProperties,
removedProperties: (!removedProperties) ? {} : removedProperties
@ -447,7 +448,7 @@ define("webodf/editor/EditorSession", [
op = new ops.OpAddParagraphStyle();
op.init({
memberid: memberid,
memberId: localMemberId,
styleName: newStyleName,
setProperties: setProperties
});
@ -460,7 +461,7 @@ define("webodf/editor/EditorSession", [
var op;
op = new ops.OpRemoveParagraphStyle();
op.init({
memberid: memberid,
memberId: localMemberId,
styleName: styleName
});
session.enqueue(op);
@ -528,6 +529,28 @@ define("webodf/editor/EditorSession", [
this.subscribe(EditorSession.signalCursorMoved, trackCursor);
/**
* @param {!function(!Object=)} callback, passing an error object in case of error
* @return {undefined}
*/
this.close = function(callback) {
self.sessionController.close(function(err) {
if (err) {
callback(err);
} else {
delete self.sessionController;
self.sessionView.close(function(err) {
if (err) {
callback(err);
} else {
delete self.sessionView;
callback();
}
});
}
});
};
function init() {
var head = document.getElementsByTagName('head')[0],
fontStyles = document.createElement('style');
@ -548,7 +571,7 @@ define("webodf/editor/EditorSession", [
/**@const*/EditorSession.signalStyleCreated = "styleCreated";
/**@const*/EditorSession.signalStyleDeleted = "styleDeleted";
/**@const*/EditorSession.signalParagraphStyleModified = "paragraphStyleModified";
/**@const*/EditorSession.signalUndoStackChanged = "signalUndoStackChanged";
/**@const*/EditorSession.signalUndoStackChanged = "signalUndoStackChanged";
return EditorSession;
});

@ -32,27 +32,27 @@
* @source: http://www.webodf.org/
* @source: http://gitorious.org/webodf/webodf/
*/
/*global define,runtime */
define("webodf/editor/MemberList",
define("webodf/editor/MemberListView",
["webodf/editor/EditorSession"],
function (EditorSession) {
"use strict";
return function MemberList(editorSession, memberListDiv) {
var self = this;
editorSession.subscribe(EditorSession.signalMemberAdded, function (memberId) {
self.addMember(memberId);
});
/**
* @param {!Element} memberListDiv
* @constructor
*/
return function MemberListView(memberListDiv) {
var editorSession = null;
editorSession.subscribe(EditorSession.signalMemberRemoved, function (memberId) {
self.removeMember(memberId);
});
runtime.assert(memberListDiv, "memberListDiv unavailable");
/**
* @param {!string} memberId
* @return {undefined}
*/
function updateAvatarButton(memberId, memberDetails) {
var node = memberListDiv.firstChild;
@ -86,9 +86,9 @@ define("webodf/editor/MemberList",
/**
* @param {!string} memberId
* @return {undefined}
*/
function createAvatarButton(memberId) {
runtime.assert(memberListDiv, "memberListDiv unavailable");
var doc = memberListDiv.ownerDocument,
htmlns = doc.documentElement.namespaceURI,
avatarDiv = doc.createElementNS(htmlns, "div"),
@ -123,6 +123,7 @@ define("webodf/editor/MemberList",
/**
* @param {!string} memberId
* @return {undefined}
*/
function removeAvatarButton(memberId) {
var node = memberListDiv.firstChild;
@ -137,18 +138,48 @@ define("webodf/editor/MemberList",
/**
* @param {!string} memberId
* @return {undefined}
*/
this.addMember = function (memberId) {
function addMember(memberId) {
createAvatarButton(memberId);
editorSession.getMemberDetailsAndUpdates(memberId, updateAvatarButton);
};
}
/**
* @param {!string} memberId
* @return {undefined}
*/
this.removeMember = function (memberId) {
function removeMember(memberId) {
editorSession.unsubscribeMemberDetailsUpdates(memberId, updateAvatarButton);
removeAvatarButton(memberId);
};
/**
* @param {!EditorSession} session
* @return {undefined}
*/
this.setEditorSession = function(session) {
var node = memberListDiv.firstChild, nextNode;
if (editorSession) {
// remove all current avatars
while (node) {
nextNode = node.nextSibling;
if (node.memberId) {
editorSession.unsubscribeMemberDetailsUpdates(node.memberId, updateAvatarButton);
}
memberListDiv.removeChild(node);
node = nextNode;
}
// unsubscribe from old editorSession
editorSession.unsubscribe(EditorSession.signalMemberAdded, addMember);
editorSession.unsubscribe(EditorSession.signalMemberRemoved, removeMember);
}
editorSession = session;
if (editorSession) {
editorSession.subscribe(EditorSession.signalMemberAdded, addMember);
editorSession.subscribe(EditorSession.signalMemberRemoved, removeMember);
}
};
};
});

@ -0,0 +1,65 @@
/**
* @license
* Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* (GNU AGPL) as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. The code is distributed
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU AGPL for more details.
*
* As additional permission under GNU AGPL version 3 section 7, you
* may distribute non-source (e.g., minimized or compacted) forms of
* that code without the copy of the GNU GPL normally required by
* section 4, provided you include this license notice and a URL
* through which recipients can access the Corresponding Source.
*
* As a special exception to the AGPL, any HTML file which merely makes function
* calls to this code, and for that purpose includes it by reference shall be
* deemed a separate work for copyright law purposes. In addition, the copyright
* holders of this code give you permission to combine this code with free
* software libraries that are released under the GNU LGPL. You may copy and
* distribute such a system following the terms of the GNU AGPL for this code
* and the LGPL for the libraries. If you modify this code, you may extend this
* exception to your version of the code, but you are not obligated to do so.
* If you do not wish to do so, delete this exception statement from your
* version.
*
* This license applies to this entire compilation.
* @licend
* @source: http://www.webodf.org/
* @source: http://gitorious.org/webodf/webodf/
*/
/*global ops, runtime */
/**
* A model which provides information about sessions.
* @interface
*/
SessionList = function SessionList() {"use strict"; };
/**
* @param {{onCreated:function(!Object),
* onUpdated:function(!Object),
* onRemoved:function(!string) }} subscriber
* @return {undefined}
*/
SessionList.prototype.getSessions = function (subscriber) {"use strict"; };
/**
* @param {{onCreated:function(!Object),
* onUpdated:function(!Object),
* onRemoved:function(!string) }} subscriber
* @return {undefined}
*/
SessionList.prototype.unsubscribe = function (subscriber) {"use strict"; };
/**
* Per default updates are enabled.
* @param {!boolean} enabled
* @return {undefined}
*/
SessionList.prototype.setUpdatesEnabled = function (enabled) {"use strict"; };

@ -59,9 +59,6 @@ define("webodf/editor/SessionListView", [], function () {
sessionDiv.sessionId = sessionDetails.id; // TODO: namespace?
sessionDiv.style.cursor = "pointer"; // TODO: do not set on each element, use CSS
sessionDiv.onclick = function () {
// HACK: stop pulling, so that does not mess up the logs
// Remove before merging to master
if (sessionList.stopPulling) { sessionList.stopPulling(); }
cb(sessionDetails.id);
};

@ -1,462 +0,0 @@
/**
* Copyright (C) 2013 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* (GNU AGPL) as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. The code is distributed
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU AGPL for more details.
*
* As additional permission under GNU AGPL version 3 section 7, you
* may distribute non-source (e.g., minimized or compacted) forms of
* that code without the copy of the GNU GPL normally required by
* section 4, provided you include this license notice and a URL
* through which recipients can access the Corresponding Source.
*
* As a special exception to the AGPL, any HTML file which merely makes function
* calls to this code, and for that purpose includes it by reference shall be
* deemed a separate work for copyright law purposes. In addition, the copyright
* holders of this code give you permission to combine this code with free
* software libraries that are released under the GNU LGPL. You may copy and
* distribute such a system following the terms of the GNU AGPL for this code
* and the LGPL for the libraries. If you modify this code, you may extend this
* exception to your version of the code, but you are not obligated to do so.
* If you do not wish to do so, delete this exception statement from your
* version.
*
* This license applies to this entire compilation.
* @licend
* @source: http://www.webodf.org/
* @source: http://gitorious.org/webodf/webodf/
*/
/*
* bootstrap the editor in different ways.
* this file is meant to be included from HTML and used
* by users who do not want to know much about the inner
* complexity.
* so we need to make it really easy.
*
* including this file will result in the namespace/object
* "webodfEditor" to be available from the HTML side.
* calling webodfEditor.boot() will start the editor.
* the method can also take some parameters to specify
* behaviour. see documentation of that method.
*
*/
/*global runtime, require, document, alert, net, window, SessionListView, ops */
// define the namespace/object we want to provide
// this is the first line of API, the user gets.
var webodfEditor = (function () {
"use strict";
runtime.currentDirectory = function () {
return "../../webodf/lib";
};
runtime.libraryPaths = function () {
return [ runtime.currentDirectory() ];
};
var editorInstance = null,
serverFactory = null,
server = null,
booting = false,
loadedFilename;
/**
* wait for a network connection through nowjs to establish.
* call the callback when done, when finally failed, or
* when a timeout reached.
* the parameter to the callback is a string with the possible
* values:
* "unavailable", "timeout", "ready"
*
* @param {!function(!string)} callback
* @return {undefined}
*/
function connectNetwork(backend, callback) {
function createServer(ServerFactory) {
serverFactory = new ServerFactory();
server = serverFactory.createServer();
server.connect(8000, callback);
}
switch (backend) {
case "pullbox":
require({ }, ["webodf/editor/server/pullbox/serverFactory"], createServer);
break;
case "nowjs":
require({ }, ["webodf/editor/server/nowjs/serverFactory"], createServer);
break;
case "owncloud":
require({ }, ["webodf/editor/server/pullbox/serverFactory"], function (ServerFactory) {
serverFactory = new ServerFactory();
server = serverFactory.createServer({url: "./documents/ajax/otpoll.php"});
server.getGenesisUrl = function(sid) {
// what a dirty hack :)
return OC.Router.generate('documents_genesis')+'/' +sid;
};
server.connect(8000, callback);
});
default:
callback("unavailable");
}
}
/**
* try to auto-sense the server-backend.
*
* NOT IMPLEMENTED / MIGHT BE NICE TO HAVE...
* for now: try to connect to pullbox backend
*
* @param {!function(!string)} callback
* @return {undefined}
*/
function detectNetwork(callback) {
connectNetwork("pullbox", callback);
}
/**
* extract document url from the url-fragment
*
* @return {?string}
*/
function guessDocUrl() {
var pos, docUrl = String(document.location);
// If the URL has a fragment (#...), try to load the file it represents
pos = docUrl.indexOf('#');
if (pos !== -1) {
docUrl = docUrl.substr(pos + 1);
} else {
docUrl = "welcome.odt";
}
return docUrl || null;
}
function fileSelectHandler(evt) {
var file, files, reader;
files = (evt.target && evt.target.files) ||
(evt.dataTransfer && evt.dataTransfer.files);
function onloadend() {
if (reader.readyState === 2) {
runtime.registerFile(file.name, reader.result);
loadedFilename = file.name;
editorInstance.loadDocument(file.name);
}
}
if (files && files.length === 1) {
file = files[0];
reader = new FileReader();
reader.onloadend = onloadend;
reader.readAsArrayBuffer(file);
} else {
alert("File could not be opened in this browser.");
}
}
function enhanceRuntime() {
var openedFiles = {},
read = runtime.read,
getFileSize = runtime.getFileSize;
runtime.read = function (path, offset, length, callback) {
var array;
if (openedFiles.hasOwnProperty(path)) {
array = new Uint8Array(openedFiles[path], offset, length);
callback(undefined, array);
} else {
return read(path, offset, length, callback);
}
};
runtime.getFileSize = function (path, callback) {
if (openedFiles.hasOwnProperty(path)) {
return callback(openedFiles[path].byteLength);
} else {
return getFileSize(path, callback);
}
};
runtime.registerFile = function (path, data) {
openedFiles[path] = data;
};
}
function createFileLoadForm() {
var form = document.createElement("form"),
input = document.createElement("input");
form.appendChild(input);
form.style.display = "none";
input.id = "fileloader";
input.setAttribute("type", "file");
input.addEventListener("change", fileSelectHandler, false);
document.body.appendChild(form);
}
function load() {
var form = document.getElementById("fileloader");
if (!form) {
enhanceRuntime();
createFileLoadForm();
form = document.getElementById("fileloader");
}
form.click();
}
function save() {
editorInstance.saveDocument(loadedFilename);
}
/**
* create a new editor instance, and start the editor with
* the given document.
*
* @param {!string} docUrl
* @param {?Object} editorOptions
* @param {?function(!Object)} editorReadyCallback
*/
function createLocalEditor(docUrl, editorOptions, editorReadyCallback) {
var pos;
booting = true;
editorOptions = editorOptions || {};
editorOptions.memberid = "localuser";
editorOptions.loadCallback = load;
editorOptions.saveCallback = save;
if (docUrl === undefined) {
docUrl = guessDocUrl();
}
runtime.assert(docUrl, "docUrl needs to be specified");
runtime.assert(editorInstance === null, "cannot boot with instanciated editor");
document.getElementById("mainContainer").style.display = "";
require({ }, ["webodf/editor/Editor"],
function (Editor) {
editorInstance = new Editor(editorOptions);
editorInstance.initAndLoadDocument(docUrl, function (editorSession) {
editorSession.startEditing();
editorReadyCallback(editorInstance);
});
}
);
}
/**
* assume the network connection is established, create a new editor instance,
* and start the editor on the network.
*
* @param {!string} sessionId
* @param {!string} memberId
* @param {?string} token
* @param {?Object} editorOptions
* @param {?function(!Object)} editorReadyCallback
*/
function createNetworkedEditor(sessionId, memberId, token, editorOptions, editorReadyCallback) {
runtime.assert(sessionId, "sessionId needs to be specified");
runtime.assert(memberId, "memberId needs to be specified");
runtime.assert(editorInstance === null, "cannot boot with instanciated editor");
editorOptions = editorOptions || {};
editorOptions.memberid = memberId;
editorOptions.networked = true;
editorOptions.networkSecurityToken = token;
require({ }, ["webodf/editor/Editor"],
function (Editor) {
// TODO: the networkSecurityToken needs to be retrieved via now.login
// (but this is to be implemented later)
editorInstance = new Editor(editorOptions, server, serverFactory);
// load the document and get called back when it's live
editorInstance.loadSession(sessionId, function (editorSession) {
editorSession.startEditing();
editorReadyCallback(editorInstance);
});
}
);
}
/**
* start the login process by offering a login/password prompt.
* the login is validated via nowjs namespace.
* on success a list of sessions is offered.
* when the user selects a session the callback is called
* with the sessionId as parameter
*
* @param {!function(!string, !string, ?string)} callback
* @returns {undefined}
*/
function startLoginProcess(callback) {
var userid, token;
booting = true;
runtime.assert(editorInstance === null, "cannot boot with instanciated editor");
function enterSession(selectedSessionId) {
document.getElementById("sessionListContainer").style.display = "none";
document.getElementById("mainContainer").style.display = "";
callback(selectedSessionId, userid, token);
}
function showSessions() {
require({ }, ["webodf/editor/SessionListView"],
function (SessionListView) {
var sessionListDiv = document.getElementById("sessionList"),
sessionList = new serverFactory.createSessionList(server),
sessionListView = new SessionListView(sessionList, sessionListDiv, enterSession);
// hide login view
document.getElementById("loginContainer").style.display = "none";
// show session list
document.getElementById("sessionListContainer").style.display = "";
}
);
}
function loginSuccess(userData) {
runtime.log("connected:" + userData.full_name);
userid = userData.uid;
token = userData.securityToken || null;
showSessions();
}
function loginFail(result) {
alert("Login failed: " + result);
}
function onLoginSubmit() {
server.login(document.loginForm.login.value, document.loginForm.password.value, loginSuccess, loginFail);
// block the submit button, we already dealt with the input
return false;
}
// bring up the login form
document.loginForm.Submit.onclick = onLoginSubmit;
document.getElementById("loginContainer").style.display = "";
}
/**
* make a guess about the document (# in URL)
* also guess about local/collaborative (depending on nowjs)
*
* @param {?Object} args
*
* args:
*
* collaborative: if set to true: connect to the network and start a
* collaborative editor. in that case the document url
* is ignored. and user needs to select a session.
*
* if set to the string "auto": it will try the above
* but fall back to non-collaborative mode [default]
*
* docUrl: if given it is used as the url to the document to load
*
* callback: callback to be called as soon as the document is loaded
*
*/
function boot(args) {
var editorOptions = {},
loginProcedure = startLoginProcess;
runtime.assert(!booting, "editor creation already in progress");
args = args || {};
if (args.collaborative === undefined) {
args.collaborative = "standalone";
} else {
args.collaborative = String(args.collaborative).toLowerCase();
}
if (args.saveCallback) {
editorOptions.saveCallback = args.saveCallback;
}
if (args.cursorAddedCallback) {
editorOptions.cursorAddedCallback = args.cursorAddedCallback;
}
if (args.cursorRemovedCallback) {
editorOptions.cursorRemovedCallback = args.cursorRemovedCallback;
}
if (args.registerCallbackForShutdown) {
editorOptions.registerCallbackForShutdown = args.registerCallbackForShutdown;
} else {
editorOptions.registerCallbackForShutdown = function (callback) {
window.onunload = callback;
};
}
if (args.loginProcedure) {
loginProcedure = args.loginProcedure;
}
// start the editor with network
function handleNetworkedSituation() {
var joinSession = server.joinSession;
if (args.joinSession) {
joinSession = args.joinSession;
}
loginProcedure(function (sessionId, userId, token) {
// if pre-authentication has happened:
if (token) {
server.setToken(token);
}
joinSession(userId, sessionId, function(memberId) {
createNetworkedEditor(sessionId, memberId, token, editorOptions, function (ed) {
if (args.callback) {
args.callback(ed);
}
});
});
});
}
// start the editor without network
function handleNonNetworkedSituation() {
createLocalEditor(args.docUrl, editorOptions, function (editor) {
if (args.callback) {
args.callback(editor);
}
});
}
if (args.collaborative === "auto") {
runtime.log("detecting network...");
detectNetwork(function (state) {
if (state === "ready") {
runtime.log("... network available.");
handleNetworkedSituation();
} else {
runtime.log("... no network available (" + state + ").");
handleNonNetworkedSituation();
}
});
} else if ((args.collaborative === "pullbox") ||
(args.collaborative === "owncloud")) {
runtime.log("starting collaborative editor for ["+args.collaborative+"].");
connectNetwork(args.collaborative, function (state) {
if (state === "ready") {
handleNetworkedSituation();
}
});
} else {
runtime.log("starting local editor.");
handleNonNetworkedSituation();
}
}
// exposed API
return { boot: boot };
}());

@ -1,5 +1,6 @@
/**
* Copyright (C) 2012 KO GmbH <copyright@kogmbh.com>
* @license
* Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* The JavaScript code in this page is free software: you can redistribute it
@ -31,14 +32,21 @@
* @source: http://www.webodf.org/
* @source: http://gitorious.org/webodf/webodf/
*/
define({
// menus
file: "Datei",
open: "Öffnen",
save: "Speichern",
edit: "Bearbeiten",
view: "Ansicht",
annotate: "Kommentieren",
clone: "Kopiere",
create: "Erzeuge",
delete: "Entferne",
insert: "Einfügen",
format: "Formatieren",
close: "Schließe",
character_DDD: "Zeichen...",
paragraph_DDD: "Absatz...",
// dialogs
@ -50,13 +58,12 @@ define({
textFlow: "Textfluß",
character: "Zeichen",
paragraphStyles: "Absatzstile",
cloneThisStyle: "Kopiere diesen Stil",
newName_C: "Neuer Name:",
// Collaboration pane
collaborationPane: "Zusammenarbeitsfeld",
people: "Leute",
chat: "Chat",
typeYourName_DDD: "Geben Sie Ihren Namen ein...",
invitePeople: "Leute einladen",
startTypingToChat_DDD: "Eingabe beginnen für Chat...",
members: "Teilnehmer",
inviteMembers: "Teilnehmer einladen",
// Various
left: "Links",
right: "Rechts",
@ -67,6 +74,8 @@ define({
spacing: "Abstand",
options: "Optionen",
style: "Stil",
undo: "Rückgängig",
redo: "Wiederherstellen",
bold: "Fett",
italic: "Kursiv",
underline: "Unterstrichen",

@ -35,11 +35,17 @@ define({
root: {
// menus
file: "File",
open: "Open",
save: "Save",
edit: "Edit",
view: "View",
annotate: "Annotate",
clone: "Clone",
create: "Create",
delete: "Delete",
insert: "Insert",
format: "Format",
close: "Close",
character_DDD: "Character...",
paragraph_DDD: "Paragraph...",
// dialogs
@ -51,13 +57,12 @@ define({
textFlow: "Text Flow",
character: "Character",
paragraphStyles: "Paragraph Styles",
cloneThisStyle: "Clone this style",
newName_C: "New name:",
// Collaboration pane
collaborationPane: "Collaboration Pane",
people: "People",
chat: "Chat",
typeYourName_DDD: "Type your name...",
invitePeople: "Invite People",
startTypingToChat_DDD: "Start typing to chat...",
members: "Members",
inviteMembers: "Invite members",
// Various
left: "Left",
right: "Right",
@ -68,6 +73,8 @@ define({
spacing: "Spacing",
options: "Options",
style: "Style",
undo: "Undo",
redo: "Redo",
bold: "Bold",
italic: "Italic",
underline: "Underline",

@ -1,124 +0,0 @@
/**
* @license
* Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* (GNU AGPL) as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. The code is distributed
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU AGPL for more details.
*
* As additional permission under GNU AGPL version 3 section 7, you
* may distribute non-source (e.g., minimized or compacted) forms of
* that code without the copy of the GNU GPL normally required by
* section 4, provided you include this license notice and a URL
* through which recipients can access the Corresponding Source.
*
* As a special exception to the AGPL, any HTML file which merely makes function
* calls to this code, and for that purpose includes it by reference shall be
* deemed a separate work for copyright law purposes. In addition, the copyright
* holders of this code give you permission to combine this code with free
* software libraries that are released under the GNU LGPL. You may copy and
* distribute such a system following the terms of the GNU AGPL for this code
* and the LGPL for the libraries. If you modify this code, you may extend this
* exception to your version of the code, but you are not obligated to do so.
* If you do not wish to do so, delete this exception statement from your
* version.
*
* This license applies to this entire compilation.
* @licend
* @source: http://www.webodf.org/
* @source: http://gitorious.org/webodf/webodf/
*/
/*global define, ops, runtime */
define("webodf/editor/server/nowjs/sessionList", [], function () {
"use strict";
return function NowjsSessionList(nowjsServer) {
var cachedSessionData = {},
subscribers = [];
function onSessionData(sessionData) {
var i,
isNew = ! cachedSessionData.hasOwnProperty(sessionData.id);
// cache
cachedSessionData[sessionData.id] = sessionData;
runtime.log("get session data for:"+sessionData.title+", is new:"+isNew);
for (i = 0; i < subscribers.length; i += 1) {
if (isNew) {
subscribers[i].onCreated(sessionData);
} else {
subscribers[i].onUpdated(sessionData);
}
}
}
function onSessionRemoved(sessionId) {
var i;
if (cachedSessionData.hasOwnProperty(sessionId)) {
delete cachedSessionData[sessionId];
for (i = 0; i < subscribers.length; i += 1) {
subscribers[i].onRemoved(sessionId);
}
}
}
this.getSessions = function (subscriber) {
var i,
sessionList = [];
if (subscriber) {
subscribers.push(subscriber);
}
for (i in cachedSessionData) {
if (cachedSessionData.hasOwnProperty(i)) {
sessionList.push(cachedSessionData[i]);
}
}
return sessionList;
};
this.unsubscribe = function (subscriber) {
var i;
for (i=0; i<subscribers.length; i+=1) {
if (subscribers[i] === subscriber) {
break;
}
}
runtime.assert((i < subscribers.length),
"tried to unsubscribe when not subscribed.");
subscribers.splice(i,1);
};
function init() {
var nowObject = nowjsServer.getNowObject();
nowObject.onSessionAdded = onSessionData;
nowObject.onSessionChanged = onSessionData;
nowObject.onSessionRemoved = onSessionRemoved;
nowObject.getSessionList( function(sessionList) {
var idx;
runtime.log("get sessions on init:"+sessionList.length);
for (idx=0; idx<sessionList.length; idx+=1) {
onSessionData(sessionList[idx])
}
});
}
init();
};
});

@ -33,33 +33,42 @@
* @source: http://gitorious.org/webodf/webodf/
*/
/*global define, document, require, runtime, core, ops */
/*global define, require, OC*/
define("webodf/editor/server/nowjs/serverFactory", [
"webodf/editor/server/nowjs/sessionList"],
function (NowjsSessionList) {
define("webodf/editor/server/owncloud/ServerFactory", [
"webodf/editor/server/pullbox/Server",
"webodf/editor/server/pullbox/MemberModel",
"webodf/editor/server/pullbox/OperationRouter",
"webodf/editor/server/pullbox/SessionList"],
function (PullBoxServer, PullBoxMemberModel, PullBoxOperationRouter, PullBoxSessionList) {
"use strict";
runtime.loadClass("ops.NowjsServer");
runtime.loadClass("ops.NowjsMemberModel");
runtime.loadClass("ops.NowjsOperationRouter");
/**
* @constructor
* @implements ServerFactory
*/
return function NowjsServerFactory() {
return function OwnCloudServerFactory() {
this.createServer = function (args) {
return new ops.NowjsServer(args);
var server;
args = args || {};
args.url = "./documents/ajax/otpoll.php";
args.sessionStateToFileUrl = OC.Router.generate('documents_session_save');
server = new PullBoxServer(args);
server.getGenesisUrl = function(sid) {
// what a dirty hack :)
return OC.Router.generate('documents_genesis') + '/' + sid;
};
return server;
};
this.createOperationRouter = function (sid, mid, server) {
return new ops.NowjsOperationRouter(sid, mid, server);
this.createOperationRouter = function (sid, mid, server, odfContainer) {
return new PullBoxOperationRouter(sid, mid, server, odfContainer);
};
this.createMemberModel = function (sid, server) {
return new ops.NowjsMemberModel(server);
return new PullBoxMemberModel(sid, server);
};
this.createSessionList = function (server) {
return new NowjsSessionList(server);
return new PullBoxSessionList(server);
};
};
});

@ -0,0 +1,281 @@
/**
* @license
* Copyright (C) 2013 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* (GNU AGPL) as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. The code is distributed
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU AGPL for more details.
*
* As additional permission under GNU AGPL version 3 section 7, you
* may distribute non-source (e.g., minimized or compacted) forms of
* that code without the copy of the GNU GPL normally required by
* section 4, provided you include this license notice and a URL
* through which recipients can access the Corresponding Source.
*
* As a special exception to the AGPL, any HTML file which merely makes function
* calls to this code, and for that purpose includes it by reference shall be
* deemed a separate work for copyright law purposes. In addition, the copyright
* holders of this code give you permission to combine this code with free
* software libraries that are released under the GNU LGPL. You may copy and
* distribute such a system following the terms of the GNU AGPL for this code
* and the LGPL for the libraries. If you modify this code, you may extend this
* exception to your version of the code, but you are not obligated to do so.
* If you do not wish to do so, delete this exception statement from your
* version.
*
* This license applies to this entire compilation.
* @licend
* @source: http://www.webodf.org/
* @source: http://gitorious.org/webodf/webodf/
*/
/*global runtime, ops*/
define("webodf/editor/server/pullbox/MemberModel", [], function () {
"use strict";
/**
* @constructor
* @implements ops.MemberModel
*/
return function PullBoxMemberModel(sessionId, server) {
var cachedMemberData = {},
memberDataSubscribers = {},
isServerPullingActivated = false,
isServerPullingOpen = true,
serverPullingTimeoutId = null,
isInstantPullingRequested = false,
isPulling = false,
/**@const*/pullingIntervall = 20000;
/**
* @param {!Object} memberData
*/
function cacheMemberDatum(memberData) {
var subscribers,
i;
// notify all subscribers who are interested in this data
subscribers = memberDataSubscribers[memberData.memberid];
if (subscribers) {
// cache
cachedMemberData[memberData.memberid] = memberData;
for (i = 0; i < subscribers.length; i += 1) {
subscribers[i](memberData.memberid, memberData);
}
}
}
function pullMemberData() {
var i,
memberIds = Object.keys(memberDataSubscribers);
if (!isServerPullingOpen || isPulling) {
return;
}
// no more timeout or instant pull request in any case
serverPullingTimeoutId = null;
isInstantPullingRequested = false;
// set lock
isPulling = true;
runtime.log("member-list request for : " + memberIds.join(","));
server.call({
command: 'query_memberdata_list',
args: {
es_id: sessionId,
member_ids: memberIds
}
}, function(responseData) {
var response = /**@type {{memberdata_list:Array.<{uid,member_id,display_name,avatar_url,color}>}}*/(runtime.fromJson(responseData)),
memberDataList,
newMemberData, oldMemberData;
// unlock
isPulling = false;
// meanwhile closed/disactivated?
if (!isServerPullingOpen || !isServerPullingActivated) {
return;
}
runtime.log("member-list reply: " + responseData);
if (response.hasOwnProperty("memberdata_list")) {
// add/update with all delivered memberdata
memberDataList = response.memberdata_list;
for (i = 0; i < memberDataList.length; i+=1) {
newMemberData = {
memberid: memberDataList[i].member_id,
fullname: memberDataList[i].display_name,
imageurl: memberDataList[i].avatar_url,
color: memberDataList[i].color
};
oldMemberData = cachedMemberData.hasOwnProperty(newMemberData.memberid) ? cachedMemberData[newMemberData.memberid] : null;
if (!oldMemberData ||
oldMemberData.fullname !== newMemberData.fullname ||
oldMemberData.imageurl !== newMemberData.imageurl ||
oldMemberData.color !== newMemberData.color) {
cacheMemberDatum(newMemberData);
}
}
} else {
runtime.log("Meh, memberdata list broken: " + responseData);
}
// trigger the next pulling
if (isInstantPullingRequested) {
pullMemberData();
} else {
serverPullingTimeoutId = runtime.setTimeout(pullMemberData, pullingIntervall);
}
});
}
/**
* Activates the pulling
* @return {undefined}
*/
function pullNewMemberData() {
// cancel any running pulling timeout
if (serverPullingTimeoutId !== null) {
runtime.clearTimeout(serverPullingTimeoutId);
}
isInstantPullingRequested = true;
isServerPullingActivated = true;
pullMemberData();
}
/**
* Deactivates the pulling if there are no more subscribers
* @return {undefined}
*/
function deactivatePeriodicMemberDataPulling() {
var key;
if (!isServerPullingActivated) {
return;
}
// check if there is no more subscription
for(key in memberDataSubscribers) {
if (memberDataSubscribers.hasOwnProperty(key)) {
// still subscribers, cannot deactivate yet
return;
}
}
isServerPullingActivated = false;
// cancel any running pulling timeout
if (serverPullingTimeoutId !== null) {
runtime.clearTimeout(serverPullingTimeoutId);
}
}
/**
* callback is called as soon as the memberdata is available and after that
* on every memberdata update.
* a parameter `null` passed to the callback means that the member is finally
* not known.
*
* @param {!string} memberId
* @param {!function(!string, ?Object)} subscriber
* @return {undefined}
*/
this.getMemberDetailsAndUpdates = function (memberId, subscriber) {
var /**@type{Object}*/
memberData = cachedMemberData[memberId],
subscribers = memberDataSubscribers[memberId] || [],
i;
memberDataSubscribers[memberId] = subscribers;
runtime.assert(subscriber !== undefined, "missing callback");
// detect double subscription
for (i=0; i<subscribers.length; i+=1) {
if (subscribers[i] === subscriber) {
break;
}
}
if (i < subscribers.length) {
// already subscribed
runtime.log("double subscription request for "+memberId+" in PullBoxMemberModel::getMemberDetailsAndUpdates");
} else {
// subscribe
subscribers.push(subscriber);
// query data from server, if not done yet
if (subscribers.length === 1) {
// TODO: only fetch data for memberId here
pullNewMemberData();
}
}
if (memberData) {
// data available from cache
subscriber(memberId, memberData);
}
};
/**
* getMemberDetailsAndUpdates subscribes a callback for updates on member details.
* this function undoes this subscription.
*
* @param {!string} memberId
* @param {!function(!string, ?Object)} subscriber
* @return {undefined}
*/
this.unsubscribeMemberDetailsUpdates = function (memberId, subscriber) {
var i,
subscribers = memberDataSubscribers[memberId];
runtime.assert(subscriber!==undefined, "missing subscriber parameter or null");
runtime.assert(subscribers,
"tried to unsubscribe when no one is subscribed ('" + memberId + "')");
if (subscribers) {
for (i=0; i<subscribers.length; i+=1) {
if (subscribers[i] === subscriber) {
break;
}
}
runtime.assert((i < subscribers.length),
"tried to unsubscribe when not subscribed for memberId '" + memberId + "'");
subscribers.splice(i,1);
// clean up
if (subscribers.length === 0) {
runtime.log("no more subscribers for: "+memberId);
delete memberDataSubscribers[memberId];
delete cachedMemberData[memberId];
deactivatePeriodicMemberDataPulling();
}
}
};
/**
* Requests a gracefull shutdown of the Member Model
* No more network activity is necessary.
*/
this.close = function (cb) {
isServerPullingOpen = false;
cb();
};
runtime.assert(server.networkStatus() === "ready", "network not ready");
};
});

@ -0,0 +1,508 @@
/**
* @license
* Copyright (C) 2013 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* (GNU AGPL) as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. The code is distributed
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU AGPL for more details.
*
* As additional permission under GNU AGPL version 3 section 7, you
* may distribute non-source (e.g., minimized or compacted) forms of
* that code without the copy of the GNU GPL normally required by
* section 4, provided you include this license notice and a URL
* through which recipients can access the Corresponding Source.
*
* As a special exception to the AGPL, any HTML file which merely makes function
* calls to this code, and for that purpose includes it by reference shall be
* deemed a separate work for copyright law purposes. In addition, the copyright
* holders of this code give you permission to combine this code with free
* software libraries that are released under the GNU LGPL. You may copy and
* distribute such a system following the terms of the GNU AGPL for this code
* and the LGPL for the libraries. If you modify this code, you may extend this
* exception to your version of the code, but you are not obligated to do so.
* If you do not wish to do so, delete this exception statement from your
* version.
*
* This license applies to this entire compilation.
* @licend
* @source: http://www.webodf.org/
* @source: http://gitorious.org/webodf/webodf/
*/
/*global runtime, ops*/
define("webodf/editor/server/pullbox/OperationRouter", [], function () {
"use strict";
runtime.loadClass("ops.OperationTransformer");
/**
* route operations in a networked collaborative manner.
*
* incoming operations (from controller) are sent to a server,
* who will distribute them.
*
* incoming operations (from the server are played on the DOM.
*/
/**
* @constructor
* @implements ops.OperationRouter
*/
return function PullBoxOperationRouter(sessionId, memberId, server, odfContainer) {
"use strict";
var operationFactory,
singleTimeCallbackOnCompleteServerOpSpecsPlay,
/**@type{function(!ops.Operation)}*/
playbackFunction,
/**@type{?{active:!boolean}}*/
pullingTimeOutFlag = null,
/**@type{!boolean}*/
triggerPushingOpsActivated = false,
/**@type{!boolean}*/
playUnplayedServerOpSpecsTriggered = false,
/**@type{!boolean}*/
syncLock = false,
/**@type{!boolean}*/
hasUnresolvableConflict = false,
/**@type{!boolean}*/
syncingBlocked = false,
/** @type {!string} id of latest op stack state known on the server */
lastServerSeq = "",
/** @type {!Array.<!Object>} ops created since the last sync call to the server */
unsyncedClientOpspecQueue = [],
/** @type {!Array.<!Object>} ops created since the last sync call to the server */
unplayedServerOpspecQueue = [],
/** @type {!Array.<!function(!boolean):undefined>} ops created since the last sync call to the server */
hasLocalUnsyncedOpsStateSubscribers = [],
/**@type{!boolean}*/
hasLocalUnsyncedOps = false,
/**@type{!boolean} tells if any local ops have been modifying ops */
hasPushedModificationOps = false,
operationTransformer = new ops.OperationTransformer(),
/**@const*/replayTime = 500,
/**@const*/pushingIntervall = 3000,
/**@const*/pullingIntervall = 8000;
function updateHasLocalUnsyncedOpsState() {
var i,
hasLocalUnsyncedOpsNow = (unsyncedClientOpspecQueue.length > 0);
// no change?
if (hasLocalUnsyncedOps === hasLocalUnsyncedOpsNow) {
return;
}
hasLocalUnsyncedOps = hasLocalUnsyncedOpsNow;
for (i=0; i<hasLocalUnsyncedOpsStateSubscribers.length; i+=1) {
hasLocalUnsyncedOpsStateSubscribers[i](hasLocalUnsyncedOps);
}
}
/**
* @param {!Array.<!Object>} opspecs
* @return {!Array.<!Object>}
*/
function compressOpSpecs(opspecs) {
var i, j, op,
result = [];
i = 0;
while (i < opspecs.length) {
// use factory to create an instance, and playback!
op = operationFactory.create(opspecs[i]);
// is known op and can do merge?
if (op !== null && op.merge) {
// go over the following and try to merge them
for (j = i+1; j < opspecs.length; j += 1) {
if (!op.merge(opspecs[j])) {
break;
}
runtime.log("Merged: "+opspecs[i].optype+" with "+opspecs[j].optype);
}
// add the resulting op to the results
result.push(op.spec());
// and continue with the one which could not be merged, or behind end
i = j;
} else {
// just pass on
result.push(opspecs[i]);
i += 1;
}
}
runtime.log("Merged: from "+opspecs.length+" to "+result.length+" specs");
return result;
}
/**
* @return {undefined}
*/
function playUnplayedServerOpSpecs() {
/**
* @return {undefined}
*/
function doPlayUnplayedServerOpSpecs() {
var opspec, op, startTime;
playUnplayedServerOpSpecsTriggered = false;
// take start time
startTime = (new Date()).getTime();
// apply as much as possible in the given time
while (unplayedServerOpspecQueue.length > 0) {
// time over?
if ((new Date().getTime()) - startTime > replayTime) {
break;
}
opspec = unplayedServerOpspecQueue.shift();
// use factory to create an instance, and playback!
op = operationFactory.create(opspec);
runtime.log(" op in: "+runtime.toJson(opspec));
if (op !== null) {
playbackFunction(op);
} else {
runtime.log("ignoring invalid incoming opspec: " + opspec);
}
}
// still unplayed opspecs?
if (unplayedServerOpspecQueue.length > 0) {
// let other events be handled. then continue
playUnplayedServerOpSpecsTriggered = true;
runtime.getWindow().setTimeout(doPlayUnplayedServerOpSpecs, 1);
} else {
// This is such a sad hack. But there is no other way for now to inject
// the callback after the initial replay.
if (singleTimeCallbackOnCompleteServerOpSpecsPlay) {
singleTimeCallbackOnCompleteServerOpSpecsPlay();
singleTimeCallbackOnCompleteServerOpSpecsPlay = null;
}
}
}
if (playUnplayedServerOpSpecsTriggered) {
return;
}
doPlayUnplayedServerOpSpecs();
}
/**
* @param {Array.<!Object>} opspecs
* @return {undefined}
*/
function receiveOpSpecsFromNetwork(opspecs) {
// append to existing unplayed
unplayedServerOpspecQueue = unplayedServerOpspecQueue.concat(opspecs);
}
/**
* Transforms the unsynced client ops and the server ops,
* applies the server ops after transformation
* @param {Array.<!Object>} serverOpspecs
* @return {!boolean}
*/
function handleOpsSyncConflict(serverOpspecs) {
var i,
transformResult;
if (! serverOpspecs) {
// TODO: proper error message, stop working
runtime.assert(false, "no opspecs received!");
return false;
} // TODO: more checking of proper content in serverOpspecs
transformResult = operationTransformer.transform(unsyncedClientOpspecQueue, /**@type{!Array.<!Object>}*/(serverOpspecs));
if (!transformResult) {
return false;
}
// store transformed server ops
for (i = 0; i < transformResult.opsB.length; i += 1) {
unplayedServerOpspecQueue.push(transformResult.opsB[i].spec());
}
// store opspecs of all transformed client opspecs
unsyncedClientOpspecQueue = [];
for (i = 0; i < transformResult.opsA.length; i += 1) {
unsyncedClientOpspecQueue.push(transformResult.opsA[i].spec());
}
return true;
}
/**
* @return {undefined}
*/
function syncOps() {
function triggerPullingOps() {
var flag = {active: true};
// provide flag globally
pullingTimeOutFlag = flag;
runtime.getWindow().setTimeout(function() {
runtime.log("Pulling activated:" + flag.active);
// remove our flag
pullingTimeOutFlag = null;
if (flag.active) {
syncOps();
}
}, pullingIntervall);
}
/**
* @return {undefined}
*/
function doSyncOps() {
var syncedClientOpspecs;
if (syncLock || hasUnresolvableConflict) {
return;
}
// TODO: hack, remove
if (syncingBlocked) {
return;
}
syncLock = true;
// take specs from queue, if any
syncedClientOpspecs = unsyncedClientOpspecQueue;
unsyncedClientOpspecQueue = [];
server.call({
command: 'sync_ops',
args: {
es_id: sessionId,
member_id: memberId,
seq_head: String(lastServerSeq),
client_ops: syncedClientOpspecs
}
}, function(responseData) {
var shouldRetryInstantly = false,
response = /** @type{{result:string, head_seq:string, ops:Array.<!Object>}} */(runtime.fromJson(responseData));
// TODO: hack, remove
if (syncingBlocked) {
return;
}
runtime.log("sync-ops reply: " + responseData);
// just new ops?
if (response.result === "new_ops") {
if (response.ops.length > 0) {
// no new locally in the meantime?
if (unsyncedClientOpspecQueue.length === 0) {
receiveOpSpecsFromNetwork(compressOpSpecs(response.ops));
} else {
// transform server ops against new local ones and apply,
// transform and send new local ops to server
runtime.log("meh, have new ops locally meanwhile, have to do transformations.");
hasUnresolvableConflict = !handleOpsSyncConflict(compressOpSpecs(response.ops));
}
// and note server state
lastServerSeq = response.head_seq;
}
} else if (response.result === "added") {
runtime.log("All added to server");
// note server state
lastServerSeq = response.head_seq;
updateHasLocalUnsyncedOpsState();
} else if (response.result === "conflict") {
// put the send ops back into the outgoing queue
unsyncedClientOpspecQueue = syncedClientOpspecs.concat(unsyncedClientOpspecQueue);
// transform server ops against new local ones and apply,
// transform and request new send new local ops to server
runtime.log("meh, server has new ops meanwhile, have to do transformations.");
hasUnresolvableConflict = !handleOpsSyncConflict(compressOpSpecs(response.ops));
// and note server state
lastServerSeq = response.head_seq;
// try again instantly
if (!hasUnresolvableConflict) {
shouldRetryInstantly = true;
}
} else {
runtime.assert(false, "Unexpected result on sync-ops call: "+response.result);
}
syncLock = false;
if (hasUnresolvableConflict) {
// TODO: offer option to reload session automatically?
runtime.assert(false,
"Sorry to tell:\n" +
"we hit a pair of operations in a state which yet need to be supported for transformation against each other.\n" +
"Client disconnected from session, no further editing accepted.\n\n" +
"Please reconnect manually for now.");
} else {
if (shouldRetryInstantly) {
doSyncOps();
} else {
runtime.log("Preparing next: " + (unsyncedClientOpspecQueue.length === 0));
// prepare next sync
// nothing to push right now?
if (unsyncedClientOpspecQueue.length === 0) {
triggerPullingOps();
}
}
playUnplayedServerOpSpecs();
}
});
}
doSyncOps();
}
function triggerPushingOps() {
if (syncLock || triggerPushingOpsActivated) {
return;
}
triggerPushingOpsActivated = true;
// disable current pulling timeout
if (pullingTimeOutFlag) {
pullingTimeOutFlag.active = false;
}
runtime.getWindow().setTimeout(function() {
runtime.log("Pushing activated");
triggerPushingOpsActivated = false;
syncOps();
}, pushingIntervall);
}
this.requestReplay = function (done_cb) {
singleTimeCallbackOnCompleteServerOpSpecsPlay = done_cb;
syncOps();
};
/**
* Sets the factory to use to create operation instances from operation specs.
*
* @param {!ops.OperationFactory} f
* @return {undefined}
*/
this.setOperationFactory = function (f) {
operationFactory = f;
operationTransformer.setOperationFactory(f);
};
/**
* Sets the method which should be called to apply operations.
*
* @param {!function(!ops.Operation)} playback_func
* @return {undefined}
*/
this.setPlaybackFunction = function (playback_func) {
playbackFunction = playback_func;
};
/**
* Brings the locally created operations into the game.
*
* @param {!ops.Operation} op
* @return {undefined}
*/
this.push = function (op) {
var timedOp,
opspec = op.spec();
if (hasUnresolvableConflict) {
return;
}
// TODO: should be an assert in the future
// there needs to be a flag telling that processing is happening,
// and thus any input should be dropped in the sessioncontroller
// ideally also have some UI element showing the processing state
if (unplayedServerOpspecQueue.length > 0) {
return;
}
// note if any local ops modified TODO: find less fragile way, perhaps have the operationFactory check it?
hasPushedModificationOps = hasPushedModificationOps || !/^(AddCursor|RemoveCursor)$/.test(opspec.optype);
// apply locally
opspec.timestamp = (new Date()).getTime();
timedOp = operationFactory.create(opspec);
playbackFunction(timedOp);
// send to server
unsyncedClientOpspecQueue.push(opspec);
triggerPushingOps();
updateHasLocalUnsyncedOpsState();
};
/**
* Requests a gracefull shutdown of the Operation Router.
* Buffered operations shall be sent to the server.
* A callback is called on success.
*/
this.close = function (cb) {
function writeSessionStateToFile() {
function cbSuccess(fileData) {
server.writeSessionStateToFile(sessionId, memberId, lastServerSeq, fileData, cb);
};
odfContainer.createByteArray(cbSuccess, cb);
}
// TODO: hack, rather add callback to syncOps for success and properly close things
syncOps();
runtime.getWindow().setTimeout(function() {
syncingBlocked = true;
if (hasPushedModificationOps) {
writeSessionStateToFile();
} else {
cb();
}
}, 2000);
};
this.getHasLocalUnsyncedOpsAndUpdates = function (subscriber) {
var i;
// detect double subscription
for (i=0; i<hasLocalUnsyncedOpsStateSubscribers.length; i+=1) {
if (subscribers[i] === subscriber) {
break;
}
}
if (i < hasLocalUnsyncedOpsStateSubscribers.length) {
// already subscribed
runtime.log("double subscription request in PullBoxMemberModel::getHasLocalUnsyncedOpsAndUpdates");
} else {
// subscribe
hasLocalUnsyncedOpsStateSubscribers.push(subscriber);
}
subscriber(hasLocalUnsyncedOps);
};
/*jslint emptyblock: true, unparam: true*/
this.unsubscribeHasLocalUnsyncedOpsUpdates = function (subscriber) {
var i;
for (i=0; i<hasLocalUnsyncedOpsStateSubscribers.length; i+=1) {
if (hasLocalUnsyncedOpsStateSubscribers[i] === subscriber) {
break;
}
}
runtime.assert((i < hasLocalUnsyncedOpsStateSubscribers.length),
"tried to unsubscribe when not subscribed in PullBoxMemberModel::getHasLocalUnsyncedOpsAndUpdates");
hasLocalUnsyncedOpsStateSubscribers.splice(i,1);
};
};
});

@ -0,0 +1,293 @@
/**
* @license
* Copyright (C) 2013 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
* (GNU AGPL) as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. The code is distributed
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU AGPL for more details.
*
* As additional permission under GNU AGPL version 3 section 7, you
* may distribute non-source (e.g., minimized or compacted) forms of
* that code without the copy of the GNU GPL normally required by
* section 4, provided you include this license notice and a URL
* through which recipients can access the Corresponding Source.
*
* As a special exception to the AGPL, any HTML file which merely makes function
* calls to this code, and for that purpose includes it by reference shall be
* deemed a separate work for copyright law purposes. In addition, the copyright
* holders of this code give you permission to combine this code with free
* software libraries that are released under the GNU LGPL. You may copy and
* distribute such a system following the terms of the GNU AGPL for this code
* and the LGPL for the libraries. If you modify this code, you may extend this
* exception to your version of the code, but you are not obligated to do so.
* If you do not wish to do so, delete this exception statement from your
* version.
*
* This license applies to this entire compilation.
* @licend
* @source: http://www.webodf.org/
* @source: http://gitorious.org/webodf/webodf/
*/
/*global XMLHttpRequest, runtime, core, ops*/
define("webodf/editor/server/pullbox/Server", [], function () {
"use strict";
runtime.loadClass("core.Base64");
runtime.loadClass("core.ByteArrayWriter");
/**
* @constructor
* @implements ops.Server
* @param {{url:string}} args
*/
return function PullBoxServer(args) {
var self = this,
token,
base64 = new core.Base64();
args = args || {};
args.url = args.url || "/WSER";
args.sessionStateToFileUrl = args.sessionStateToFileUrl || "/SS2F"
this.getGenesisUrl = function (sessionId) {
return "/session/" + sessionId + "/genesis";
};
/**
* @param {!Object} message
* @param {!function(!string)} cb
* @return {undefined}
*/
function call(message, cb) {
var xhr = new XMLHttpRequest(),
byteArrayWriter = new core.ByteArrayWriter("utf8"),
messageString = JSON.stringify(message),
data;
function handleResult() {
if (xhr.readyState === 4) {
if ((xhr.status < 200 || xhr.status >= 300) && xhr.status === 0) {
// report error
runtime.log("Status " + String(xhr.status) + ": " +
xhr.responseText || xhr.statusText);
}
cb(xhr.responseText);
}
}
runtime.log("Sending message to server: "+messageString);
// create body data for request from metadata and payload
byteArrayWriter.appendString(messageString);
// byteArrayWriter.appendByteArray(zipData);
data = byteArrayWriter.getByteArray();
// do the request
xhr.open('POST', args.url, true);
xhr.onreadystatechange = handleResult;
// ArrayBufferView will have an ArrayBuffer property, in WebKit, XHR can send()
// an ArrayBuffer, In Firefox, one must use sendAsBinary with a string
if (data.buffer && !xhr.sendAsBinary) {
data = data.buffer; // webkit supports sending an ArrayBuffer
} else {
// encode into a string, this works in FireFox >= 3
data = runtime.byteArrayToString(data, "binary");
}
try {
if (xhr.sendAsBinary) {
xhr.sendAsBinary(data);
} else {
xhr.send(data);
}
} catch (e) {
runtime.log("Problem with calling server: " + e + " " + data);
cb(e.message);
}
}
this.call = call;
this.getToken = function () {
return token;
};
/**
* for pre-authenticated use
*/
this.setToken = function (a_token) {
token = a_token;
};
/*jslint unparam: true*/
/**
* @param {!number} timeout in milliseconds
* @param {!function(!string)} callback
* @return {undefined}
*/
this.connect = function (timeout, callback) {
/*
var accumulatedWaitingTime = 0;
// already tried connecting?
if (self.networkStatus() === "ready") {
return;
}
*/
callback("ready");
};
/*jslint unparam: false*/
/**
* @return {!string}
*/
this.networkStatus = function () {
return "ready";
};
/**
* @param {!string} login
* @param {!string} password
* @param {function(!Object)} successCb
* @param {function(!string)} failCb
* @return {undefined}
*/
this.login = function (login, password, successCb, failCb) {
call({
command: "login",
args: {
login: base64.toBase64(login),
password: base64.toBase64(password)
}
}, function(responseData) {
var response = /**@type {{token:string}}*/(runtime.fromJson(responseData));
runtime.log("Login reply: " + responseData);
if (response.hasOwnProperty("token")) {
token = response.token;
runtime.log("Caching token: " + self.getToken());
successCb(response);
} else {
failCb(responseData);
}
});
};
/**
* @param {!string} userId
* @param {!string} sessionId
* @param {!function(!string)} successCb
* @param {function()=} failCb
* @return {undefined}
*/
this.joinSession = function (userId, sessionId, successCb, failCb) {
call({
command: "join_session",
args: {
user_id: userId,
es_id: sessionId
}
}, function(responseData) {
var response = /**@type {{success:string, member_id:string}}*/(runtime.fromJson(responseData));
runtime.log("join_session reply: " + responseData);
if (response.hasOwnProperty("success") && response.success) {
successCb(response.member_id);
} else {
if (failCb) {
failCb();
}
}
});
};
/**
* @param {!string} sessionId
* @param {!string} memberId
* @param {!function()} successCb
* @param {function()=} failCb
* @return {undefined}
*/
this.leaveSession = function (sessionId, memberId, successCb, failCb) {
call({
command: "leave_session",
args: {
es_id: sessionId,
member_id: memberId
}
}, function(responseData) {
var response = /**@type {{success:string, member_id:string}}*/(runtime.fromJson(responseData));
runtime.log("leave_session reply: " + responseData);
if (response.hasOwnProperty("success") && response.success) {
successCb();
} else {
if (failCb) {
failCb();
}
}
});
};
/**
* @param {!string} sessionId
* @param {!string} memberId
* @param {!string} seqHead
* @param {function()=} callback
* @return {undefined}
*/
this.writeSessionStateToFile = function(sessionId, memberId, seqHead, fileData, callback) {
// code copied from BrowserRuntime.writeFile and adapted
var xhr = new XMLHttpRequest();
function handleResult() {
if (xhr.readyState === 4) {
if (xhr.status === 0 && !xhr.responseText) {// TODO: check makes sense here as well?
// for local files there is no difference between missing
// and empty files, so empty files are considered as errors
runtime.log("File " + args.sessionStateToFileUrl + " is empty.");
} else if ((xhr.status >= 200 && xhr.status < 300) ||
xhr.status === 0) {
// report success
runtime.log(null);
} else {
// report error
runtime.log("Status " + String(xhr.status) + ": " +
xhr.responseText || xhr.statusText);
}
callback();
}
}
// do the request
xhr.open('POST', args.sessionStateToFileUrl, true);
xhr.setRequestHeader("x-webodf-es_id", sessionId);
xhr.setRequestHeader("x-webodf-member_id", memberId);
xhr.setRequestHeader("x-webodf-seq_head", seqHead);
xhr.onreadystatechange = handleResult;
// ArrayBufferView will have an ArrayBuffer property, in WebKit, XHR can send()
// an ArrayBuffer, In Firefox, one must use sendAsBinary with a string
if (fileData.buffer && !xhr.sendAsBinary) {
fileData = fileData.buffer; // webkit supports sending an ArrayBuffer
} else {
// encode into a string, this works in FireFox >= 3
fileData = runtime.byteArrayToString(fileData, "binary");
}
try {
if (xhr.sendAsBinary) {
xhr.sendAsBinary(fileData);
} else {
xhr.send(fileData);
}
} catch (e) {
runtime.log("Problem with calling \"writeSessionStateToFile\" on server");
callback(e.message);
}
};
};
});

@ -33,30 +33,29 @@
* @source: http://gitorious.org/webodf/webodf/
*/
/*global define, document, require, runtime, core, ops */
/*global define, document, require, runtime, ops */
define("webodf/editor/server/pullbox/serverFactory", [
"webodf/editor/server/pullbox/sessionList"],
function (PullBoxSessionList) {
define("webodf/editor/server/pullbox/ServerFactory", [
"webodf/editor/server/pullbox/Server",
"webodf/editor/server/pullbox/MemberModel",
"webodf/editor/server/pullbox/OperationRouter",
"webodf/editor/server/pullbox/SessionList"],
function (PullBoxServer, PullBoxMemberModel, PullBoxOperationRouter, PullBoxSessionList) {
"use strict";
runtime.loadClass("ops.PullBoxServer");
runtime.loadClass("ops.PullBoxMemberModel");
runtime.loadClass("ops.PullBoxOperationRouter");
/**
* @constructor
* @implements ServerFactory
*/
return function PullBoxServerFactory() {
this.createServer = function (args) {
return new ops.PullBoxServer(args);
return new PullBoxServer(args);
};
this.createOperationRouter = function (sid, mid, server) {
return new ops.PullBoxOperationRouter(sid, mid, server);
this.createOperationRouter = function (sid, mid, server, odfContainer) {
return new PullBoxOperationRouter(sid, mid, server, odfContainer);
};
this.createMemberModel = function (sid, server) {
return new ops.PullBoxMemberModel(sid, server);
return new PullBoxMemberModel(sid, server);
};
this.createSessionList = function (server) {
return new PullBoxSessionList(server);

@ -35,14 +35,13 @@
/*global define, ops, runtime */
define("webodf/editor/server/pullbox/sessionList", [], function () {
define("webodf/editor/server/pullbox/SessionList", [], function () {
"use strict";
return function PullBoxSessionList(server) {
var cachedSessionData = {},
subscribers = [],
/** HACK: allow to stop pulling, so that does not mess up the logs
* Remove before merging to master */
serverPullingTimeoutId = null,
pullingActive = true;
function onSessionData(sessionData) {
@ -75,7 +74,7 @@ define("webodf/editor/server/pullbox/sessionList", [], function () {
}
function pullSessionList() {
if (!pullingActive) { return; }
serverPullingTimeoutId = null;
server.call({
command: "query_sessiondata_list"
@ -84,6 +83,11 @@ define("webodf/editor/server/pullbox/sessionList", [], function () {
sessionList, i,
unupdatedSessions = {};
// stopped meanwhile? TODO: support for cancelling calls
if (!pullingActive) {
return;
}
runtime.log("query_sessiondata_list reply: " + responseData);
if (response.hasOwnProperty("sessiondata_list")) {
@ -111,7 +115,7 @@ define("webodf/editor/server/pullbox/sessionList", [], function () {
}
// next update in 5 secs
runtime.getWindow().setTimeout(pullSessionList, 5000);
serverPullingTimeoutId = runtime.getWindow().setTimeout(pullSessionList, 5000);
} else {
runtime.log("Meh, sessionlist data broken: " + responseData);
}
@ -150,8 +154,21 @@ define("webodf/editor/server/pullbox/sessionList", [], function () {
subscribers.splice(i,1);
};
this.stopPulling = function () {
pullingActive = false;
this.setUpdatesEnabled = function (enabled) {
if (pullingActive === enabled) {
return;
}
pullingActive = enabled;
if (pullingActive) {
pullSessionList();
} else {
// cancel any running pulling timeout
if (serverPullingTimeoutId !== null) {
runtime.clearTimeout(serverPullingTimeoutId);
serverPullingTimeoutId = null;
}
}
};
function init() {

@ -31,122 +31,180 @@
* @source: http://www.webodf.org/
* @source: http://gitorious.org/webodf/webodf/
*/
/*global define,document,require */
define("webodf/editor/widgets", [
"dojo/ready",
"dijit/MenuItem",
"dijit/DropDownMenu",
"dijit/form/Button",
"dijit/form/DropDownButton",
"dijit/Toolbar",
"webodf/editor/widgets/simpleStyles",
"webodf/editor/widgets/undoRedoMenu",
"webodf/editor/widgets/toolbarWidgets/currentStyle",
"webodf/editor/widgets/paragraphStylesDialog",
"webodf/editor/widgets/zoomSlider"],
function (SimpleStyles, UndoRedoMenu, CurrentStyle, ParagraphStylesDialog, ZoomSlider) {
function (ready, MenuItem, DropDownMenu, Button, DropDownButton, Toolbar, SimpleStyles, UndoRedoMenu, CurrentStyle, ParagraphStylesDialog, ZoomSlider) {
"use strict";
return function loadWidgets(editorSession, loadOdtFile, saveOdtFile) {
var translator = document.translator;
// Menubar
require([
"dojo/ready",
"dijit/MenuItem",
"dijit/DropDownMenu",
"dijit/form/Button",
"dijit/form/DropDownButton",
"dijit/Toolbar"
], function (ready, MenuItem, DropDownMenu, Button, DropDownButton, Toolbar) {
ready(function () {
var loadButton, saveButton, dropDownMenu, menuButton, paragraphStylesMenuItem, dialog, toolbar, simpleStyles, currentStyle, zoomSlider,
undoRedoMenu, annotateButton;
dropDownMenu = new DropDownMenu({});
paragraphStylesMenuItem = new MenuItem({
label: translator("paragraph_DDD")
});
dropDownMenu.addChild(paragraphStylesMenuItem);
dialog = new ParagraphStylesDialog(editorSession, function (dialog) {
paragraphStylesMenuItem.onClick = function () {
dialog.startup();
dialog.show();
};
});
return function ToolBarTools(args) {
var translator = document.translator,
loadOdtFile = args.loadOdtFile,
saveOdtFile = args.saveOdtFile,
close = args.close,
toolbar,
loadButton, saveButton, annotateButton, closeButton,
formatDropDownMenu, formatMenuButton,
paragraphStylesMenuItem, paragraphStylesDialog, simpleStyles, currentStyle,
zoomSlider,
undoRedoMenu,
editorSession;
// Toolbar
toolbar = new Toolbar({}, "toolbar");
this.setEditorSession = function(session) {
editorSession = session;
if (undoRedoMenu) {
undoRedoMenu.setEditorSession(session);
}
if (simpleStyles) {
simpleStyles.setEditorSession(session);
}
if (currentStyle) {
currentStyle.setEditorSession(session);
}
if (zoomSlider) {
zoomSlider.setEditorSession(session);
}
if (paragraphStylesDialog) {
paragraphStylesDialog.setEditorSession(session);
}
};
if (editorSession.hasUndoManager()) {
undoRedoMenu = new UndoRedoMenu(editorSession, function (widget) {
widget.placeAt(toolbar);
widget.startup();
});
}
// init
ready(function () {
toolbar = new Toolbar({}, "toolbar");
// Simple Style Selector [B, I, U, S]
simpleStyles = new SimpleStyles(editorSession, function (widget) {
// Undo/Redo
if (args.undoRedoEnabled) {
undoRedoMenu = new UndoRedoMenu(function (widget) {
widget.placeAt(toolbar);
widget.startup();
});
undoRedoMenu.setEditorSession(editorSession);
}
// Paragraph Style Selector
currentStyle = new CurrentStyle(editorSession, function (widget) {
if (args.directStylingEnabled) {
// Simple Style Selector [B, I, U, S]
simpleStyles = new SimpleStyles(function (widget) {
widget.placeAt(toolbar);
widget.startup();
});
simpleStyles.setEditorSession(editorSession);
}
// Zoom Level Selector
zoomSlider = new ZoomSlider(editorSession, function (widget) {
widget.placeAt(toolbar);
widget.startup();
});
// Paragraph Style Selector
currentStyle = new CurrentStyle(function (widget) {
widget.placeAt(toolbar);
widget.startup();
});
currentStyle.setEditorSession(editorSession);
if (loadOdtFile) {
loadButton = new Button({
label: translator('open'),
showLabel: false,
iconClass: 'dijitIcon dijitIconFolderOpen',
style: {
float: 'left'
},
onClick: function () {
loadOdtFile();
}
});
loadButton.placeAt(toolbar);
}
if (saveOdtFile) {
saveButton = new Button({
label: translator('save'),
showLabel: false,
iconClass: 'dijitEditorIcon dijitEditorIconSave',
style: {
float: 'left'
},
onClick: function () {
saveOdtFile();
}
});
saveButton.placeAt(toolbar);
}
// Zoom Level Selector
zoomSlider = new ZoomSlider(function (widget) {
widget.placeAt(toolbar);
widget.startup();
});
zoomSlider.setEditorSession(editorSession);
// Load
if (loadOdtFile) {
loadButton = new Button({
label: translator('open'),
showLabel: false,
iconClass: 'dijitIcon dijitIconFolderOpen',
style: {
float: 'left'
},
onClick: function () {
loadOdtFile();
}
});
loadButton.placeAt(toolbar);
}
menuButton = new DropDownButton({
dropDown: dropDownMenu,
label: translator('format'),
iconClass: "dijitIconEditTask",
// Save
if (saveOdtFile) {
saveButton = new Button({
label: translator('save'),
showLabel: false,
iconClass: 'dijitEditorIcon dijitEditorIconSave',
style: {
float: 'left'
},
onClick: function () {
saveOdtFile();
}
});
menuButton.placeAt(toolbar);
saveButton.placeAt(toolbar);
}
// Format menu
formatDropDownMenu = new DropDownMenu({});
paragraphStylesMenuItem = new MenuItem({
label: translator("paragraph_DDD")
});
formatDropDownMenu.addChild(paragraphStylesMenuItem);
paragraphStylesDialog = new ParagraphStylesDialog(function (dialog) {
paragraphStylesMenuItem.onClick = function () {
if (editorSession) {
dialog.startup();
dialog.show();
}
};
});
paragraphStylesDialog.setEditorSession(editorSession);
formatMenuButton = new DropDownButton({
dropDown: formatDropDownMenu,
label: translator('format'),
iconClass: "dijitIconEditTask",
style: {
float: 'left'
}
});
formatMenuButton.placeAt(toolbar);
// Add annotation
if (args.annotationsEnabled) {
annotateButton = new Button({
label: translator('annotate'),
showLabel: false,
iconClass: 'dijitIconBookmark',
onClick: function () {
editorSession.addAnnotation();
if (editorSession) {
editorSession.addAnnotation();
}
}
});
annotateButton.placeAt(toolbar);
});
}
if (close) {
closeButton = new Button({
label: translator('close'),
showLabel: false,
iconClass: 'dijitEditorIcon dijitEditorIconCancel',
style: {
float: 'right'
},
onClick: function () {
close();
}
});
closeButton.placeAt(toolbar);
}
});
};

@ -1,5 +1,7 @@
/**
* @license
* Copyright (C) 2013 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
@ -30,14 +32,16 @@
* @source: http://www.webodf.org/
* @source: http://gitorious.org/webodf/webodf/
*/
/*global runtime,core,define,require,document,dijit */
runtime.loadClass("core.CSSUnits");
define("webodf/editor/widgets/dialogWidgets/alignmentPane", [], function () {
"use strict";
var AlignmentPane = function (editorSession, callback) {
var AlignmentPane = function (callback) {
var self = this,
editorSession,
contentPane,
form;
@ -83,6 +87,10 @@ define("webodf/editor/widgets/dialogWidgets/alignmentPane", [], function () {
}
};
this.setEditorSession = function(session) {
editorSession = session;
};
function init(cb) {
require([
"dojo",

@ -1,5 +1,7 @@
/**
* @license
* Copyright (C) 2013 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
@ -30,12 +32,15 @@
* @source: http://www.webodf.org/
* @source: http://gitorious.org/webodf/webodf/
*/
/*global runtime,define,require,document,dijit */
define("webodf/editor/widgets/dialogWidgets/fontEffectsPane", [], function () {
"use strict";
var FontEffectsPane = function (editorSession, callback) {
var FontEffectsPane = function (callback) {
var self = this,
editorSession,
contentPane,
form,
preview,
@ -148,10 +153,11 @@ define("webodf/editor/widgets/dialogWidgets/fontEffectsPane", [], function () {
backgroundColorTB.set('value', value);
};
fontPicker = new FontPicker(editorSession, function (picker) {
fontPicker = new FontPicker(function (picker) {
picker.widget().startup();
document.getElementById('fontPicker').appendChild(picker.widget().domNode);
picker.widget().name = 'fontName';
picker.setEditorSession(editorSession);
});
// Automatically update preview when selections change
@ -184,6 +190,13 @@ define("webodf/editor/widgets/dialogWidgets/fontEffectsPane", [], function () {
});
}
this.setEditorSession = function(session) {
editorSession = session;
if (fontPicker) {
fontPicker.setEditorSession(editorSession);
}
};
init(function () {
return callback(self);
});

@ -1,5 +1,6 @@
/**
* Copyright (C) 2012 KO GmbH <copyright@kogmbh.com>
* @license
* Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* The JavaScript code in this page is free software: you can redistribute it
@ -31,7 +32,9 @@
* @source: http://www.webodf.org/
* @source: http://gitorious.org/webodf/webodf/
*/
/*global define,require */
define("webodf/editor/widgets/paragraphStyles",
["webodf/editor/EditorSession"],
@ -40,8 +43,9 @@ define("webodf/editor/widgets/paragraphStyles",
/**
* @constructor
*/
var ParagraphStyles = function (editorSession, callback) {
var ParagraphStyles = function (callback) {
var self = this,
editorSession,
select;
this.widget = function () {
@ -61,9 +65,14 @@ define("webodf/editor/widgets/paragraphStyles",
this.onRemove = null;
function populateStyles() {
var i, availableStyles, selectionList;
var i, selectionList, availableStyles;
if (! select) {
return;
}
selectionList = [];
availableStyles = editorSession.getAvailableParagraphStyles();
availableStyles = editorSession ? editorSession.getAvailableParagraphStyles() : [];
for (i = 0; i < availableStyles.length; i += 1) {
selectionList.push({
@ -76,10 +85,34 @@ define("webodf/editor/widgets/paragraphStyles",
select.addOption(selectionList);
}
function addStyle(newStyleName) {
var stylens = "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
newStyleElement = editorSession.getParagraphStyleElement(newStyleName);
if (select) {
select.addOption({
value: newStyleName,
label: newStyleElement.getAttributeNS(stylens, 'display-name')
});
}
if (self.onAdd) {
self.onAdd(newStyleName);
}
}
function removeStyle(styleName) {
if (select) {
select.removeOption(styleName);
}
if (self.onRemove) {
self.onRemove(styleName);
}
}
function init(cb) {
require(["dijit/form/Select"], function (Select) {
var stylens = "urn:oasis:names:tc:opendocument:xmlns:style:1.0";
select = new Select({
name: 'ParagraphStyles',
maxHeight: 200,
@ -90,29 +123,24 @@ define("webodf/editor/widgets/paragraphStyles",
populateStyles();
editorSession.subscribe(EditorSession.signalStyleCreated, function (newStyleName) {
var newStyleElement = editorSession.getParagraphStyleElement(newStyleName);
select.addOption({
value: newStyleName,
label: newStyleElement.getAttributeNS(stylens, 'display-name')
});
if (self.onAdd) {
self.onAdd(newStyleName);
}
});
editorSession.subscribe(EditorSession.signalStyleDeleted, function (styleName) {
select.removeOption(styleName);
if (self.onRemove) {
self.onRemove(styleName);
}
});
return cb();
});
}
this.setEditorSession = function(session) {
if (editorSession) {
editorSession.unsubscribe(EditorSession.signalStyleCreated, addStyle);
editorSession.unsubscribe(EditorSession.signalStyleDeleted, removeStyle);
}
editorSession = session;
if (editorSession) {
editorSession.subscribe(EditorSession.signalStyleCreated, addStyle);
editorSession.subscribe(EditorSession.signalStyleDeleted, removeStyle);
populateStyles();
}
};
// init
init(function () {
return callback(self);
});

@ -1,5 +1,6 @@
/**
* Copyright (C) 2012 KO GmbH <copyright@kogmbh.com>
* @license
* Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* The JavaScript code in this page is free software: you can redistribute it
@ -31,278 +32,300 @@
* @source: http://www.webodf.org/
* @source: http://gitorious.org/webodf/webodf/
*/
/*global define,require,document,dojo,dijit */
define("webodf/editor/widgets/paragraphStylesDialog", [], function () {
"use strict";
function makeWidget(editorSession, callback) {
require([
"dijit/Dialog",
"dijit/TooltipDialog",
"dijit/popup",
"dijit/layout/TabContainer",
"dijit/layout/ContentPane",
"dijit/form/Button",
"dijit/form/DropDownButton",
"dijit/form/RadioButton"], function (Dialog, TooltipDialog, popup, TabContainer, ContentPane, Button, DropDownButton, RadioButton) {
var i,
dialog,
translator = document.translator,
tabContainer,
alignmentPane,
fontEffectsPane,
stylePicker,
flowPane,
numberingPane,
tabsPane,
capsPane,
bordersPane,
backgroundPane,
indentsPane,
actionBar,
okButton,
cancelButton,
cloneButton,
deleteButton,
cloneTooltip,
cloneDropDown,
newStyleName = null,
/**
* Mapping of the properties from edit pane properties to the attributes of style:text-properties
* @const@type{Array.<!{propertyName:string,attributeName:string,unit:string}>}
*/
textPropertyMapping = [
{
propertyName: 'fontSize',
attributeName: 'fo:font-size',
unit: 'pt'
}, {
propertyName: 'fontName',
attributeName: 'style:font-name'
}, {
propertyName: 'color',
attributeName: 'fo:color'
}, {
propertyName: 'backgroundColor',
attributeName: 'fo:background-color'
}, {
propertyName: 'fontWeight',
attributeName: 'fo:font-weight'
}, {
propertyName: 'fontStyle',
attributeName: 'fo:font-style'
}, {
propertyName: 'underline',
attributeName: 'style:text-underline-style'
}, {
propertyName: 'strikethrough',
attributeName: 'style:text-line-through-style'
}],
return function ParagraphStylesDialog(callback) {
var editorSession,
dialog,
stylePicker, alignmentPane, fontEffectsPane;
function makeWidget(callback) {
require([
"dijit/Dialog",
"dijit/TooltipDialog",
"dijit/popup",
"dijit/layout/TabContainer",
"dijit/layout/ContentPane",
"dijit/form/Button",
"dijit/form/DropDownButton",
"dijit/form/RadioButton"], function (Dialog, TooltipDialog, popup, TabContainer, ContentPane, Button, DropDownButton, RadioButton) {
var i,
translator = document.translator,
tabContainer,
flowPane,
numberingPane,
tabsPane,
capsPane,
bordersPane,
backgroundPane,
indentsPane,
actionBar,
okButton,
cancelButton,
cloneButton,
deleteButton,
cloneTooltip,
cloneDropDown,
newStyleName = null,
/**
* Mapping of the properties from edit pane properties to the attributes of style:text-properties
* @const@type{Array.<!{propertyName:string,attributeName:string,unit:string}>}
*/
textPropertyMapping = [
{
propertyName: 'fontSize',
attributeName: 'fo:font-size',
unit: 'pt'
}, {
propertyName: 'fontName',
attributeName: 'style:font-name'
}, {
propertyName: 'color',
attributeName: 'fo:color'
}, {
propertyName: 'backgroundColor',
attributeName: 'fo:background-color'
}, {
propertyName: 'fontWeight',
attributeName: 'fo:font-weight'
}, {
propertyName: 'fontStyle',
attributeName: 'fo:font-style'
}, {
propertyName: 'underline',
attributeName: 'style:text-underline-style'
}, {
propertyName: 'strikethrough',
attributeName: 'style:text-line-through-style'
}],
/**
* Mapping of the properties from edit pane properties to the attributes of style:paragraph-properties
* @const@type{Array.<!{propertyName:string,attributeName:string,unit:string}>}
*/
paragraphPropertyMapping = [
{
propertyName: 'topMargin',
attributeName: 'fo:margin-top',
unit: 'mm'
}, {
propertyName: 'bottomMargin',
attributeName: 'fo:margin-bottom',
unit: 'mm'
}, {
propertyName: 'leftMargin',
attributeName: 'fo:margin-left',
unit: 'mm'
}, {
propertyName: 'rightMargin',
attributeName: 'fo:margin-right',
unit: 'mm'
}, {
propertyName: 'textAlign',
attributeName: 'fo:text-align'
}];
/**
* Mapping of the properties from edit pane properties to the attributes of style:paragraph-properties
* @const@type{Array.<!{propertyName:string,attributeName:string,unit:string}>}
* Sets attributes of a node by the properties of the object properties,
* based on the mapping defined in propertyMapping.
* @param {!Object} properties
* @param {!Array.<!{propertyName:string,attributeName:string,unit:string}>} propertyMapping
* @return {undefined}
*/
paragraphPropertyMapping = [
{
propertyName: 'topMargin',
attributeName: 'fo:margin-top',
unit: 'mm'
}, {
propertyName: 'bottomMargin',
attributeName: 'fo:margin-bottom',
unit: 'mm'
}, {
propertyName: 'leftMargin',
attributeName: 'fo:margin-left',
unit: 'mm'
}, {
propertyName: 'rightMargin',
attributeName: 'fo:margin-right',
unit: 'mm'
}, {
propertyName: 'textAlign',
attributeName: 'fo:text-align'
}];
/**
* Sets attributes of a node by the properties of the object properties,
* based on the mapping defined in propertyMapping.
* @param {!Object} properties
* @param {!Array.<!{propertyName:string,attributeName:string,unit:string}>} propertyMapping
* @return {undefined}
*/
function mappedProperties(properties, propertyMapping) {
var i, m, value,
result = {};
for (i = 0; i < propertyMapping.length; i += 1) {
m = propertyMapping[i];
value = properties[m.propertyName];
// Set a value as the attribute of a node, if that value is defined.
// If there is a unit specified, it is suffixed to the value.
if (value !== undefined) {
result[m.attributeName] = (m.unit !== undefined) ? value + m.unit : value;
function mappedProperties(properties, propertyMapping) {
var i, m, value,
result = {};
for (i = 0; i < propertyMapping.length; i += 1) {
m = propertyMapping[i];
value = properties[m.propertyName];
// Set a value as the attribute of a node, if that value is defined.
// If there is a unit specified, it is suffixed to the value.
if (value !== undefined) {
result[m.attributeName] = (m.unit !== undefined) ? value + m.unit : value;
}
}
return result;
}
return result;
}
function accept() {
editorSession.updateParagraphStyle(stylePicker.value(), {
"style:paragraph-properties": mappedProperties(alignmentPane.value(), paragraphPropertyMapping),
"style:text-properties": mappedProperties(fontEffectsPane.value(), textPropertyMapping)
});
dialog.hide();
}
function accept() {
editorSession.updateParagraphStyle(stylePicker.value(), {
"style:paragraph-properties": mappedProperties(alignmentPane.value(), paragraphPropertyMapping),
"style:text-properties": mappedProperties(fontEffectsPane.value(), textPropertyMapping)
});
function cancel() {
dialog.hide();
}
/**
* Creates and enqueues a paragraph-style cloning operation.
* Remembers the id of the created style in newStyleName, so the
* style picker can be set to it, once the operation has been applied.
* @param {!string} styleName id of the style to clone
* @param {!string} newStyleDisplayName display name of the new style
*/
function cloneStyle(styleName, newStyleDisplayName) {
newStyleName = editorSession.cloneParagraphStyle(styleName, newStyleDisplayName);
}
dialog.hide();
}
function deleteStyle(styleName) {
editorSession.deleteStyle(styleName);
}
// Dialog
dialog = new Dialog({
title: translator("paragraphStyles")
});
function cancel() {
dialog.hide();
}
cloneTooltip = new TooltipDialog({
content:
'<h2 style="margin: 0;">Clone this style</h2><br/>' +
'<label for="name">New name </label><input data-dojo-type="dijit/form/TextBox" id="name" name="name"><br/><br/>',
style: "width: 300px;"
});
cloneButton = new Button({
label: 'Create',
onClick: function () {
cloneStyle(stylePicker.value(), cloneTooltip.get('value').name);
cloneTooltip.reset();
popup.close(cloneTooltip);
/**
* Creates and enqueues a paragraph-style cloning operation.
* Remembers the id of the created style in newStyleName, so the
* style picker can be set to it, once the operation has been applied.
* @param {!string} styleName id of the style to clone
* @param {!string} newStyleDisplayName display name of the new style
*/
function cloneStyle(styleName, newStyleDisplayName) {
newStyleName = editorSession.cloneParagraphStyle(styleName, newStyleDisplayName);
}
});
cloneTooltip.addChild(cloneButton);
cloneDropDown = new DropDownButton({
label: 'Clone',
showLabel: false,
iconClass: 'dijitEditorIcon dijitEditorIconCopy',
dropDown: cloneTooltip,
style: "float: right; margin-bottom: 5px;"
});
dialog.addChild(cloneDropDown, 1);
deleteButton = new Button({
label: 'Delete',
showLabel: false,
iconClass: 'dijitEditorIcon dijitEditorIconDelete',
style: "float: right; margin-bottom: 5px;",
onClick: function () {
deleteStyle(stylePicker.value());
function deleteStyle(styleName) {
editorSession.deleteStyle(styleName);
}
});
dialog.addChild(deleteButton, 2);
// Dialog
dialog = new Dialog({
title: translator("paragraphStyles")
});
// Tab Container
tabContainer = new TabContainer({
style: "height: 100%; width: 100%;"
});
dialog.addChild(tabContainer, 3);
cloneTooltip = new TooltipDialog({
content:
'<h2 style="margin: 0;">'+translator("cloneThisStyle")+'</h2><br/>' +
'<label for="name">'+translator("newName_C")+'</label> <input data-dojo-type="dijit/form/TextBox" id="name" name="name"><br/><br/>',
style: "width: 300px;"
});
cloneButton = new Button({
label: translator("create"),
onClick: function () {
cloneStyle(stylePicker.value(), cloneTooltip.get('value').name);
cloneTooltip.reset();
popup.close(cloneTooltip);
}
});
cloneTooltip.addChild(cloneButton);
cloneDropDown = new DropDownButton({
label: translator("clone"),
showLabel: false,
iconClass: 'dijitEditorIcon dijitEditorIconCopy',
dropDown: cloneTooltip,
style: "float: right; margin-bottom: 5px;"
});
dialog.addChild(cloneDropDown, 1);
actionBar = dojo.create("div", {
"class": "dijitDialogPaneActionBar"
});
okButton = new dijit.form.Button({
label: translator("ok"),
onClick: accept
}).placeAt(actionBar);
cancelButton = new dijit.form.Button({
label: translator("cancel"),
onClick: cancel
}).placeAt(actionBar);
dialog.domNode.appendChild(actionBar);
deleteButton = new Button({
label: translator("delete"),
showLabel: false,
iconClass: 'dijitEditorIcon dijitEditorIconDelete',
style: "float: right; margin-bottom: 5px;",
onClick: function () {
deleteStyle(stylePicker.value());
}
});
dialog.addChild(deleteButton, 2);
// Tab Container
tabContainer = new TabContainer({
style: "height: 100%; width: 100%;"
});
dialog.addChild(tabContainer, 3);
require([
"webodf/editor/widgets/paragraphStyles",
"webodf/editor/widgets/dialogWidgets/alignmentPane",
"webodf/editor/widgets/dialogWidgets/fontEffectsPane"
], function (ParagraphStyles, AlignmentPane, FontEffectsPane) {
var p, a, f;
actionBar = dojo.create("div", {
"class": "dijitDialogPaneActionBar"
});
okButton = new dijit.form.Button({
label: translator("ok"),
onClick: accept
}).placeAt(actionBar);
cancelButton = new dijit.form.Button({
label: translator("cancel"),
onClick: cancel
}).placeAt(actionBar);
dialog.domNode.appendChild(actionBar);
function openStyle(value) {
alignmentPane.setStyle(value);
fontEffectsPane.setStyle(value);
if (editorSession.isStyleUsed(editorSession.getParagraphStyleElement(value))) {
deleteButton.domNode.style.display = 'none';
} else {
deleteButton.domNode.style.display = 'block';
}
}
p = new ParagraphStyles(editorSession, function (paragraphStyles) {
stylePicker = paragraphStyles;
stylePicker.widget().startup();
stylePicker.widget().domNode.style.float = "left";
stylePicker.widget().domNode.style.width = "350px";
stylePicker.widget().domNode.style.marginTop = "5px";
dialog.addChild(stylePicker.widget(), 0);
require([
"webodf/editor/widgets/paragraphStyles",
"webodf/editor/widgets/dialogWidgets/alignmentPane",
"webodf/editor/widgets/dialogWidgets/fontEffectsPane"
], function (ParagraphStyles, AlignmentPane, FontEffectsPane) {
var p, a, f;
stylePicker.onAdd = function (name) {
if (newStyleName === name) {
stylePicker.setValue(name);
newStyleName = null; // reset 'flag' name
function openStyle(value) {
alignmentPane.setStyle(value);
fontEffectsPane.setStyle(value);
if (editorSession.isStyleUsed(editorSession.getParagraphStyleElement(value))) {
deleteButton.domNode.style.display = 'none';
} else {
deleteButton.domNode.style.display = 'block';
}
};
}
stylePicker.onRemove = function (name) {
// Set the first style name as current
stylePicker.setValue(stylePicker.widget().getOptions(0));
};
p = new ParagraphStyles(function (paragraphStyles) {
stylePicker = paragraphStyles;
stylePicker.widget().startup();
stylePicker.widget().domNode.style.float = "left";
stylePicker.widget().domNode.style.width = "350px";
stylePicker.widget().domNode.style.marginTop = "5px";
dialog.addChild(stylePicker.widget(), 0);
stylePicker.widget().onChange = openStyle;
});
a = new AlignmentPane(editorSession, function (pane) {
alignmentPane = pane;
alignmentPane.widget().startup();
tabContainer.addChild(alignmentPane.widget());
});
f = new FontEffectsPane(editorSession, function (pane) {
fontEffectsPane = pane;
fontEffectsPane.widget().startup();
tabContainer.addChild(fontEffectsPane.widget());
stylePicker.onAdd = function (name) {
if (newStyleName === name) {
stylePicker.setValue(name);
newStyleName = null; // reset 'flag' name
}
};
stylePicker.onRemove = function (name) {
// Set the first style name as current
stylePicker.setValue(stylePicker.widget().getOptions(0));
};
stylePicker.widget().onChange = openStyle;
stylePicker.setEditorSession(editorSession);
});
a = new AlignmentPane(function (pane) {
alignmentPane = pane;
alignmentPane.widget().startup();
tabContainer.addChild(alignmentPane.widget());
alignmentPane.setEditorSession(editorSession);
});
f = new FontEffectsPane(function (pane) {
fontEffectsPane = pane;
fontEffectsPane.widget().startup();
tabContainer.addChild(fontEffectsPane.widget());
fontEffectsPane.setEditorSession(editorSession);
});
dialog.onShow = function () {
var currentStyle = editorSession.getCurrentParagraphStyle();
// setting the stylepicker value if the style name is the same
// will not trigger onChange, so specifically open the style in
// the panes.
if (stylePicker.value() === currentStyle) {
openStyle(currentStyle);
} else {
stylePicker.setValue(currentStyle);
}
};
});
dialog.onShow = function () {
var currentStyle = editorSession.getCurrentParagraphStyle();
// setting the stylepicker value if the style name is the same
// will not trigger onChange, so specifically open the style in
// the panes.
if (stylePicker.value() === currentStyle) {
openStyle(currentStyle);
} else {
stylePicker.setValue(currentStyle);
}
};
});
tabContainer.startup();
tabContainer.startup();
return callback(dialog);
});
}
return callback(dialog);
});
}
this.setEditorSession = function(session) {
editorSession = session;
if (stylePicker) {
stylePicker.setEditorSession(session);
}
if (alignmentPane) {
alignmentPane.setEditorSession(session);
}
if (fontEffectsPane) {
fontEffectsPane.setEditorSession(session);
}
if (!editorSession && dialog) { // TODO: check show state
dialog.hide();
}
};
return function ParagraphStylesDialog(editorSession, callback) {
makeWidget(editorSession, function (dialog) {
// init
makeWidget(function (dialog) {
return callback(dialog);
});
};

@ -1,5 +1,6 @@
/**
* Copyright (C) 2012 KO GmbH <copyright@kogmbh.com>
* @license
* Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* The JavaScript code in this page is free software: you can redistribute it
@ -31,113 +32,145 @@
* @source: http://www.webodf.org/
* @source: http://gitorious.org/webodf/webodf/
*/
/*global define,require,document */
define("webodf/editor/widgets/simpleStyles",
["webodf/editor/EditorSession"],
function (EditorSession) {
"use strict";
function makeWidget(editorSession, callback) {
require(["dijit/form/ToggleButton"], function (ToggleButton) {
var i,
widget = {},
boldButton,
italicButton,
underlineButton,
strikethroughButton;
return function SimpleStyles(callback) {
var editorSession,
boldButton,
italicButton,
underlineButton,
strikethroughButton;
boldButton = new ToggleButton({
label: document.translator('bold'),
showLabel: false,
checked: false,
iconClass: "dijitEditorIcon dijitEditorIconBold",
onChange: function (checked) {
var value = checked ? 'bold' : 'normal';
editorSession.formatSelection({
'style:text-properties': {
'fo:font-weight' : value
function makeWidget(callback) {
require(["dijit/form/ToggleButton"], function (ToggleButton) {
var i,
widget = {};
boldButton = new ToggleButton({
label: document.translator('bold'),
showLabel: false,
checked: editorSession ? editorSession.isBold(): false,
iconClass: "dijitEditorIcon dijitEditorIconBold",
onChange: function (checked) {
var value = checked ? 'bold' : 'normal';
if (editorSession) {
editorSession.formatSelection({
'style:text-properties': {
'fo:font-weight' : value
}
});
}
});
}
});
}
});
italicButton = new ToggleButton({
label: document.translator('italic'),
showLabel: false,
checked: false,
iconClass: "dijitEditorIcon dijitEditorIconItalic",
onChange: function (checked) {
var value = checked ? 'italic' : 'normal';
editorSession.formatSelection({
'style:text-properties': {
'fo:font-style' : value
italicButton = new ToggleButton({
label: document.translator('italic'),
showLabel: false,
checked: editorSession ? editorSession.isItalic(): false,
iconClass: "dijitEditorIcon dijitEditorIconItalic",
onChange: function (checked) {
var value = checked ? 'italic' : 'normal';
if (editorSession) {
editorSession.formatSelection({
'style:text-properties': {
'fo:font-style' : value
}
});
}
});
}
});
underlineButton = new ToggleButton({
label: document.translator('underline'),
showLabel: false,
checked: false,
iconClass: "dijitEditorIcon dijitEditorIconUnderline",
onChange: function (checked) {
var value = checked ? 'solid' : 'none';
editorSession.formatSelection({
'style:text-properties': {
'style:text-underline-style' : value
}
});
underlineButton = new ToggleButton({
label: document.translator('underline'),
showLabel: false,
checked: editorSession ? editorSession.hasUnderline(): false,
iconClass: "dijitEditorIcon dijitEditorIconUnderline",
onChange: function (checked) {
var value = checked ? 'solid' : 'none';
if (editorSession) {
editorSession.formatSelection({
'style:text-properties': {
'style:text-underline-style' : value
}
});
}
});
}
});
strikethroughButton = new ToggleButton({
label: document.translator('strikethrough'),
showLabel: false,
checked: false,
iconClass: "dijitEditorIcon dijitEditorIconStrikethrough",
onChange: function (checked) {
var value = checked ? 'solid' : 'none';
editorSession.formatSelection({
'style:text-properties': {
'style:text-line-through-style' : value
}
});
strikethroughButton = new ToggleButton({
label: document.translator('strikethrough'),
showLabel: false,
checked: editorSession ? editorSession.hasStrikeThrough(): false,
iconClass: "dijitEditorIcon dijitEditorIconStrikethrough",
onChange: function (checked) {
var value = checked ? 'solid' : 'none';
if (editorSession) {
editorSession.formatSelection({
'style:text-properties': {
'style:text-line-through-style' : value
}
});
}
}
});
widget.children = [boldButton, italicButton, underlineButton, strikethroughButton];
widget.startup = function () {
widget.children.forEach(function (element) {
element.startup();
});
};
widget.placeAt = function (container) {
widget.children.forEach(function (element) {
element.placeAt(container);
});
}
return widget;
};
return callback(widget);
});
function checkStyleButtons() {
// The 3rd parameter is false to avoid firing onChange when setting the value
// programmatically.
}
function checkStyleButtons() {
// The 3rd parameter is false to avoid firing onChange when setting the value
// programmatically.
if (boldButton) {
boldButton.set('checked', editorSession.isBold(), false);
}
if (italicButton) {
italicButton.set('checked', editorSession.isItalic(), false);
}
if (underlineButton) {
underlineButton.set('checked', editorSession.hasUnderline(), false);
}
if (strikethroughButton) {
strikethroughButton.set('checked', editorSession.hasStrikeThrough(), false);
}
}
editorSession.subscribe(EditorSession.signalCursorMoved, checkStyleButtons);
editorSession.subscribe(EditorSession.signalParagraphChanged, checkStyleButtons);
editorSession.subscribe(EditorSession.signalParagraphStyleModified, checkStyleButtons);
widget.children = [boldButton, italicButton, underlineButton, strikethroughButton];
widget.startup = function () {
widget.children.forEach(function (element) {
element.startup();
});
};
widget.placeAt = function (container) {
widget.children.forEach(function (element) {
element.placeAt(container);
});
return widget;
};
return callback(widget);
});
}
this.setEditorSession = function(session) {
if (editorSession) {
editorSession.unsubscribe(EditorSession.signalCursorMoved, checkStyleButtons);
editorSession.unsubscribe(EditorSession.signalParagraphChanged, checkStyleButtons);
editorSession.unsubscribe(EditorSession.signalParagraphStyleModified, checkStyleButtons);
}
editorSession = session;
if (editorSession) {
editorSession.subscribe(EditorSession.signalCursorMoved, checkStyleButtons);
editorSession.subscribe(EditorSession.signalParagraphChanged, checkStyleButtons);
editorSession.subscribe(EditorSession.signalParagraphStyleModified, checkStyleButtons);
checkStyleButtons();
}
};
return function SimpleStyles(editorSession, callback) {
makeWidget(editorSession, function (widget) {
// init
makeWidget(function (widget) {
return callback(widget);
});
};

@ -1,5 +1,6 @@
/**
* Copyright (C) 2012 KO GmbH <copyright@kogmbh.com>
* @license
* Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* The JavaScript code in this page is free software: you can redistribute it
@ -31,35 +32,63 @@
* @source: http://www.webodf.org/
* @source: http://gitorious.org/webodf/webodf/
*/
/*global define, require */
define("webodf/editor/widgets/toolbarWidgets/currentStyle",
["webodf/editor/EditorSession"],
function (EditorSession) {
"use strict";
function makeWidget(editorSession, callback) {
require(["webodf/editor/widgets/paragraphStyles"], function (ParagraphStyles) {
var paragraphStyles, widget;
paragraphStyles = new ParagraphStyles(editorSession, function (pStyles) {
// if the current paragraph style changes, update the widget
editorSession.subscribe(EditorSession.signalParagraphChanged, function (info) {
if (info.type === 'style') {
pStyles.widget().set("value", info.styleName);
}
});
return function CurrentStyle(callback) {
var editorSession,
paragraphStyles;
function selectParagraphStyle(info) {
if (paragraphStyles) {
if (info.type === 'style') {
paragraphStyles.widget().set("value", info.styleName);
}
}
}
function setParagraphStyle(value) {
if (editorSession) {
editorSession.setCurrentParagraphStyle(value);
}
}
pStyles.widget().onChange = function (value) {
editorSession.setCurrentParagraphStyle(value);
};
function makeWidget(callback) {
require(["webodf/editor/widgets/paragraphStyles"], function (ParagraphStyles) {
var p;
return callback(pStyles.widget());
p = new ParagraphStyles(function (pStyles) {
paragraphStyles = pStyles;
paragraphStyles.widget().onChange = setParagraphStyle;
paragraphStyles.setEditorSession(editorSession);
return callback(paragraphStyles.widget());
});
});
});
}
}
this.setEditorSession = function(session) {
if (editorSession) {
editorSession.unsubscribe(EditorSession.signalParagraphChanged, selectParagraphStyle);
}
editorSession = session;
if (paragraphStyles) {
paragraphStyles.setEditorSession(editorSession);
}
if (editorSession) {
editorSession.subscribe(EditorSession.signalParagraphChanged, selectParagraphStyle);
// TODO: selectParagraphStyle(editorSession.getCurrentParagraphStyle());
}
};
return function CurrentStyle(editorSession, callback) {
makeWidget(editorSession, function (widget) {
makeWidget(function (widget) {
return callback(widget);
});
};

@ -1,5 +1,6 @@
/**
* Copyright (C) 2012 KO GmbH <copyright@kogmbh.com>
* @license
* Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* The JavaScript code in this page is free software: you can redistribute it
@ -31,66 +32,88 @@
* @source: http://www.webodf.org/
* @source: http://gitorious.org/webodf/webodf/
*/
/*global define,require,document */
define("webodf/editor/widgets/undoRedoMenu",
["webodf/editor/EditorSession"],
function (EditorSession) {
"use strict";
function makeWidget(editorSession, callback) {
require(["dijit/form/Button"], function (Button) {
var widget = {},
undoButton,
redoButton;
return function UndoRedoMenu(callback) {
var editorSession,
undoButton,
redoButton;
undoButton = new Button({
label: document.translator('undo'),
showLabel: false,
disabled: true,
iconClass: "dijitEditorIcon dijitEditorIconUndo",
onClick: function () {
editorSession.undo();
}
});
function makeWidget(callback) {
require(["dijit/form/Button"], function (Button) {
var widget = {};
undoButton = new Button({
label: document.translator('undo'),
showLabel: false,
disabled: true, // TODO: get current session state
iconClass: "dijitEditorIcon dijitEditorIconUndo",
onClick: function () {
if (editorSession) {
editorSession.undo();
}
}
});
redoButton = new Button({
label: document.translator('redo'),
showLabel: false,
disabled: true,
iconClass: "dijitEditorIcon dijitEditorIconRedo",
onClick: function () {
editorSession.redo();
}
redoButton = new Button({
label: document.translator('redo'),
showLabel: false,
disabled: true, // TODO: get current session state
iconClass: "dijitEditorIcon dijitEditorIconRedo",
onClick: function () {
if (editorSession) {
editorSession.redo();
}
}
});
widget.children = [undoButton, redoButton];
widget.startup = function () {
widget.children.forEach(function (element) {
element.startup();
});
};
widget.placeAt = function (container) {
widget.children.forEach(function (element) {
element.placeAt(container);
});
return widget;
};
return callback(widget);
});
}
function checkUndoButtons(e) {
function checkUndoButtons(e) {
if (undoButton) {
undoButton.set('disabled', e.undoAvailable === false);
}
if (redoButton) {
redoButton.set('disabled', e.redoAvailable === false);
}
}
editorSession.subscribe(EditorSession.signalUndoStackChanged, checkUndoButtons);
widget.children = [undoButton, redoButton];
widget.startup = function () {
widget.children.forEach(function (element) {
element.startup();
});
};
widget.placeAt = function (container) {
widget.children.forEach(function (element) {
element.placeAt(container);
});
return widget;
};
return callback(widget);
});
}
this.setEditorSession = function(session) {
if (editorSession) {
editorSession.unsubscribe(EditorSession.signalUndoStackChanged, checkUndoButtons);
}
editorSession = session;
if (editorSession) {
editorSession.subscribe(EditorSession.signalUndoStackChanged, checkUndoButtons);
// TODO: checkUndoButtons(editorSession.getundoredoavailablalalo());
}
};
return function UndoRedoMenu(editorSession, callback) {
makeWidget(editorSession, function (widget) {
// init
makeWidget(function (widget) {
return callback(widget);
});
};

@ -1,6 +1,7 @@
/**
* Copyright (C) 2012 KO GmbH <copyright@kogmbh.com>
* @license
* Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* The JavaScript code in this page is free software: you can redistribute it
* and/or modify it under the terms of the GNU Affero General Public License
@ -31,39 +32,51 @@
* @source: http://www.webodf.org/
* @source: http://gitorious.org/webodf/webodf/
*/
/*global define,require,document */
define("webodf/editor/widgets/zoomSlider", [], function () {
"use strict";
function makeWidget(editorSession, callback) {
require(["dijit/form/HorizontalSlider", "dijit/form/NumberTextBox", "dojo"], function (HorizontalSlider, NumberTextBox, dojo) {
var widget = {},
canvas;
widget = new HorizontalSlider({
name: 'zoomSlider',
value: 100,
minimum: 30,
maximum: 150,
discreteValues: 100,
intermediateChanges: true,
style: {
width: '150px',
height: '25px',
float: 'right'
}
});
return function ZoomSlider(callback) {
var editorSession,
slider;
canvas = dojo.byId('canvas');
widget.onChange = function (value) {
editorSession.getOdfCanvas().setZoomLevel(value / 100.0);
};
function makeWidget(callback) {
require(["dijit/form/HorizontalSlider", "dijit/form/NumberTextBox", "dojo"], function (HorizontalSlider, NumberTextBox, dojo) {
var widget = {};
return callback(widget);
});
}
slider = new HorizontalSlider({
name: 'zoomSlider',
value: 100,
minimum: 30,
maximum: 150,
discreteValues: 100,
intermediateChanges: true,
style: {
width: '150px',
height: '25px',
float: 'right'
}
});
slider.onChange = function (value) {
if (editorSession) {
editorSession.getOdfCanvas().setZoomLevel(value / 100.0);
}
};
return callback(slider);
});
}
this.setEditorSession = function(session) {
editorSession = session;
// if (slider) { slider.setValue(editorSession.getOdfCanvas().getZoomLevel() ); TODO!
};
return function ZoomSlider(editorSession, callback) {
makeWidget(editorSession, function (widget) {
// init
makeWidget(function (widget) {
return callback(widget);
});
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save