/** | |
* @fileOverview PersistentStorage PersistentStorage.js file. | |
* This plugin is a rethinking of the ExtendFileManager plugin written and | |
* designed by Wei Zhuo, Afru, Krzysztof Kotowicz, Raimund Meyer. | |
* | |
* @author Douglas Mayle <douglas@openplans.org> | |
* @version 1.0 | |
*/ | |
/** | |
* The global namespace | |
* @name window | |
*/ | |
(function($) { | |
/** | |
* @name PersistentStorage | |
* @class Provides an interface for persistant storage options to the user. | |
* @param editor A reference to the Xinha Editor this plugin instance is attached to. | |
*/ | |
var PersistentStorage = window.PersistentStorage = function (editor) { | |
/** | |
* @memberOf Xinha | |
*/ | |
this.editor = editor; | |
var self = this; | |
} | |
/** | |
* Plugin Metadata | |
* @namespace | |
* @static | |
*/ | |
PersistentStorage._pluginInfo = { | |
/** Plugin name */ | |
name : "PersistentStorage", | |
/** Plugin version */ | |
version : "1.0", | |
/** Plugin author */ | |
developer : "Douglas Mayle", | |
/** Plugin author's website */ | |
developer_url : "http://douglas.mayle.org", | |
/** Plugin license */ | |
license : "BSD" | |
}; | |
/** | |
* The list of backend modules registered with this instance. | |
*/ | |
PersistentStorage.prototype._backends = {}; | |
/** | |
* The list of user config files received from backends. | |
*/ | |
PersistentStorage.prototype._userconfigs = []; | |
/** | |
* The name of the currently active backend. | |
*/ | |
PersistentStorage.prototype._activeBackend = ''; | |
/** | |
* The value of the type filter that's currently in effect. | |
*/ | |
PersistentStorage.prototype._typeFilter = ''; | |
/** | |
* The currently displayed view type. | |
*/ | |
PersistentStorage.prototype._viewType = 'thumbnail'; | |
/** | |
* On Xinha activation, this get's called so that we can prepare any resources | |
* we need to function. | |
*/ | |
PersistentStorage.prototype.onGenerateOnce = function () { | |
// We use _prepareDialog to asynchronously load the dialog markup and then | |
// perform necessary processing. | |
this._prepareDialog(); | |
}; | |
function addClass(element, className) { | |
var classRegex = new RegExp(' ?' + className + ' ?'); | |
if (!classRegex.test(element.className)) { | |
element.className += ' ' + className; | |
} | |
} | |
function removeClass(element, className) { | |
var classRegex = new RegExp(' ?' + className + ' ?'); | |
if (classRegex.test(element.className)) { | |
element.className = element.className.replace(classRegex, ' '); | |
} | |
} | |
function toggleClass(element, className) { | |
var classRegex = new RegExp(' ?' + className + ' ?'); | |
if (classRegex.test(element.className)) { | |
element.className = element.className.replace(classRegex, ' '); | |
} else { | |
element.className += ' ' + className; | |
} | |
} | |
/** | |
* Once we're sure we have a backend that supports document functionality, | |
* we'll load the interface that exposes it. | |
*/ | |
PersistentStorage.prototype._registerDocumentUI = function () { | |
if (this._documentEnabled) { | |
// No need to repeated rebuild the UI. | |
return; | |
} | |
this._documentEnabled = true; | |
var self = this; | |
var editor = this.editor; | |
editor.config.registerButton({ | |
id : 'newdocument', | |
tooltip : Xinha._lc( 'New Document', 'PersistentStorage' ), | |
image : [_editor_url + editor.config.imgURL + 'ed_buttons_main.png',0,5], | |
textMode : true, | |
action : function() { self.newDocument(); } | |
} | |
); | |
editor.config.registerButton({ | |
id : 'opendocument', | |
tooltip : Xinha._lc( 'Open Document', 'PersistentStorage' ), | |
image : [_editor_url + editor.config.imgURL + 'ed_buttons_main.png',1,5], | |
textMode : true, | |
action : function() { self.openDialog(); } | |
} | |
); | |
editor.config.registerButton({ | |
id : 'savedocument', | |
tooltip : Xinha._lc( 'Save Document', 'PersistentStorage' ), | |
image : [_editor_url + editor.config.imgURL + 'ed_buttons_main.png',9,1], | |
textMode : true, | |
action : function() { self.saveDialog(); } | |
} | |
); | |
editor.config.addToolbarElement('newdocument', 'fullscreen', 1); | |
editor.config.addToolbarElement('opendocument', 'newdocument', 1); | |
editor.config.addToolbarElement('savedocument', 'opendocument', 1); | |
// Since this is after the editor load, we have to trigger an udate manually... | |
editor._rebuildToolbar(); | |
}; | |
/** | |
* Backend storage plugins should call this method so that they can be exposed | |
* to the user. Because of possible quirks in plugin loading, you should use | |
* the following example code when registering. | |
* @param name The name of the module | |
* @param module The module instance | |
* @param config Configuration information that tells us about the module | |
* @param user_config AN object representing the user configuration loaded from | |
* this module. | |
* @example | |
* PSBackend.prototype.onGenerateOnce = function () { | |
* // Register with the Persistent Storage plugin. | |
* this._registerBackend(); | |
* }; | |
* PSBackend.prototype._registerBackend = function(timeWaited) { | |
* var editor = this.editor; | |
* var self = this; | |
* | |
* if (!timeWaited) { | |
* timeWaited = 0; | |
* } | |
* | |
* // Retry over a period of ten seconds to register. We back off exponentially | |
* // to limit resouce usage in the case of misconfiguration. | |
* var registerTimeout = 10000; | |
* | |
* if (timeWaited > registerTimeout) { | |
* // This is most likely a configuration error. We're loaded and | |
* // PersistentStorage is not. | |
* return; | |
* } | |
* | |
* if (!editor.plugins['PersistentStorage'] || | |
* !editor.plugins['PersistentStorage'].instance || | |
* !editor.plugins['PersistentStorage'].instance.ready) { | |
* | |
* window.setTimeout(function() { | |
* self._registerBackend(timeWaited ? timeWaited*2 : 50); | |
* }, timeWaited ? timeWaited : 50); | |
* | |
* return; | |
* } | |
* var PS = editor.plugins['PersistentStorage'].instance; | |
* | |
* // Support user configuration. This loading should be moved into PersistentStorage... | |
* this.loadDocument({URL:'', name:'config.js', key:'/config.js'}, function(json) { | |
* var userconfig = json ? eval('(' + json + ')') : false; | |
* PS.registerBackend('PSLocal', self, self.config, userconfig); | |
* }); | |
*/ | |
PersistentStorage.prototype.registerBackend = function (name, module, config, user_config) { | |
this._backends[name] = {module: module, config: config, name: name}; | |
// TODO I'd like something more than just whoever calls back first wins for ordering. | |
if (!this._activeBackend) { | |
this.setBackend(name); | |
} | |
if (config.capabilities.upload_operations && | |
config.capabilities.file_operations) { | |
this._registerDocumentUI(); | |
} | |
// Handle user configuration | |
if (user_config) { | |
this._userconfigs.push(user_config); | |
this.configureUser(); | |
} | |
this.updatePlacesDisplay(); | |
} | |
/** | |
* Go through the list of user configs and reconfigure Xinha. | |
*/ | |
PersistentStorage.prototype.configureUser = function () { | |
// Temp code does not handle user affinity | |
var self = this; | |
for (var index=0; index<this._userconfigs.length; ++index) { | |
// A user config can have various options, we'll deal with each in turn. | |
var config = this._userconfigs[index]; | |
for (var pluginName in (config.plugins || {})) { | |
// Try to load each of the plugins requested by the user. | |
Xinha.loadPlugin(pluginName, function() { | |
// On successful load, we'll register the plugin with this instance. | |
self.editor.registerPlugins([pluginName]); | |
Xinha.refreshPlugin(self.editor.plugins[pluginName].instance); | |
// Now that | |
self.editor._rebuildToolbar(); | |
}, config.plugins[pluginName]); | |
} | |
} | |
} | |
/** | |
* Present the user with a clean slate document. Someday, we'll cycle through | |
* a list of templates in storage. | |
*/ | |
PersistentStorage.prototype.newDocument = function () { | |
if (this.editor._doc.body.lastChild) { | |
// There's already some content, let's make sure. | |
if (!confirm(Xinha._lc('This will erase any unsaved content. If you\'re certain, please click OK to continue.', 'PersistentStorage'))) { | |
return; | |
} | |
} | |
while (this.editor._doc.body.lastChild) { | |
this.editor._doc.body.removeChild(this.editor._doc.body.lastChild); | |
} | |
} | |
/** | |
* Present a save dialog to the user. | |
*/ | |
PersistentStorage.prototype.saveDialog = function () { | |
var self = this; | |
// Setup the dialog title | |
var title = this.dialog.getElementById("h1"); | |
title.innerHTML = Xinha._lc('Save Document', 'PersistentStorage'); | |
// Setup the confirmation button and action. | |
var button = this.dialog.getElementById("confirm"); | |
button.value = Xinha._lc('Save', 'PersistentStorage'); | |
button.onclick = function() { | |
self.saveDocument(); | |
}; | |
// Enable and disable the buttons with filename input | |
var nameinput = this.dialog.getElementById("filename"); | |
button.disabled = nameinput.value ? false : true; | |
nameinput.onkeyup = nameinput.onkeypress = nameinput.onchange = function() { | |
button.disabled = nameinput.value ? false : true; | |
} | |
this.showDialog({typeFilter:['document','html','text','folder'], styleFor:'documentsave'}); | |
} | |
/** | |
* Use the selected backend to save the current document. | |
*/ | |
PersistentStorage.prototype.saveDocument = function () { | |
var editor = this.editor; | |
var module = this._backends[this._activeBackend].module; | |
var filename = this.dialog.getElementById("filename"); | |
module.saveDocument(module.viewFilter, filename.value, editor.getHTML(), function() { | |
module.cache = null; | |
}); | |
this.dialog.hide(); | |
} | |
/** | |
* Present an open dialog to the user. | |
*/ | |
PersistentStorage.prototype.openDialog = function () { | |
var self = this; | |
// Setup the dialog title | |
var title = this.dialog.getElementById("h1"); | |
title.innerHTML = Xinha._lc('Open Document', 'PersistentStorage'); | |
// Setup the confirmation button and action. | |
var button = this.dialog.getElementById("confirm"); | |
button.value = Xinha._lc('Open', 'PersistentStorage'); | |
button.onclick = function() { | |
self.openDocument(); | |
}; | |
this.showDialog({typeFilter:['document','html','text','folder'], styleFor:'documentload'}); | |
} | |
/** | |
* Use the selected backend to load the selected entry into the editor. | |
*/ | |
PersistentStorage.prototype.openDocument = function () { | |
var editor = this.editor; | |
var module = this._backends[this._activeBackend].module; | |
module.loadDocument(this.selectedEntry[0], function(documentSource) { | |
editor.setHTML(documentSource); | |
}); | |
this.dialog.hide(); | |
} | |
/** | |
* @param options Configuration options that tweak the display of the dialog. | |
* @param options.typeFilter {String[]} Object types to allow in the display. | |
* Possibilities are 'image', 'media', 'document', and 'folder'. | |
* @param options.styleFor {String} Possible stylings of the dialog. This can | |
* be 'insertion', 'link', 'documentsave', or 'documentload'. | |
* @example | |
* Interface Mockup | |
* | |
* **************************************************************************** | |
* * * | |
* * #Places## #File Browser################################################# * | |
* * # # # ______________ ________________ ________________ # * | |
* * # === # # |/subdir | ^(Up) |*Create Folder| |View as Icons^| # * | |
* * # |*| # # -------------- ---------------- ---------------- # * | |
* * # |*| # # # * | |
* * # === # # *********** *********** *********** *********** # * | |
* * #Server # # * Thumb * * Thumb * * Thumb * * Thumb * # * | |
* * # # # * * * * * * * * # * | |
* * # /\ # # *(Edit) * *(Edit) * *(Edit) * *(Edit) * # * | |
* * # < > # # *(Delete) * *(Delete) * *(Delete) * *(Delete) * # * | |
* * # \/ # # *********** *********** *********** *********** # * | |
* * # Gears # # # * | |
* * # # ############################################################## * | |
* * # # * | |
* * # # *Import-(Collapsed)******************************************* * | |
* * # # * | |
* * # # #File Name -or- File Details################################## * | |
* * # # # # * | |
* * # # # # * | |
* * # # # # * | |
* * # # # # * | |
* * ######### ############################################################## * | |
* * * | |
* **************************************************************************** | |
*/ | |
PersistentStorage.prototype.showDialog = function (options) { | |
// Create a reference to this PS instance to allow for asynchronous | |
// continuation (if we're not ready.) | |
var self = this; | |
if (!this.ready) { | |
window.setTimeout(function() {self.showDialog(options);}, 80); | |
return; | |
} | |
// We hide and show the various elements of the dialog depending on usage. | |
removeClass(this.dialog.getElementById("WWW"), 'hidden'); | |
addClass(this.dialog.getElementById("namefield"), 'hidden'); | |
addClass(this.dialog.getElementById("placeWww"), 'hidden'); | |
addClass(this.dialog.getElementById("placeBackend"), 'hidden'); | |
switch (options.styleFor) { | |
case 'link': | |
removeClass(this.dialog.getElementById("placeWww"), 'hidden'); | |
this.updatePlacesDisplay('shared_publish'); | |
break; | |
case 'insertion': | |
removeClass(this.dialog.getElementById("placeBackend"), 'hidden'); | |
this.updatePlacesDisplay('shared_publish'); | |
break; | |
case 'documentsave': | |
addClass(this.dialog.getElementById("WWW"), 'hidden'); | |
removeClass(this.dialog.getElementById("namefield"), 'hidden'); | |
removeClass(this.dialog.getElementById("placeBackend"), 'hidden'); | |
this.updatePlacesDisplay('file_operations'); | |
break; | |
case 'documentload': | |
addClass(this.dialog.getElementById("WWW"), 'hidden'); | |
removeClass(this.dialog.getElementById("placeBackend"), 'hidden'); | |
this.updatePlacesDisplay('file_operations'); | |
break; | |
} | |
var directories = this.dialog.getElementById("filters"); | |
var fileBrowser = this.dialog.getElementById("fileList"); | |
// Store the type filter so that view updates will correctly filter the contents. | |
this._typeFilter = options.typeFilter; | |
var module = this._backends[this._activeBackend].module; | |
this.updateFileBrowser(); | |
} | |
/** | |
* Take the list of filters and build the contents of the select element. | |
* @param filters {Filter[]} An array of filter choices to display to the user. | |
* @param filters[n].value The unique key that represents this filter to the backend. | |
* @param filters[n].display {String} A text string to display to the user to | |
* represent the given filter. | |
* @param select {HTMLElement} The select node to update. | |
*/ | |
PersistentStorage.prototype.displayFilters = function(filters, viewFilter) { | |
// Clear out the previous directory listing. | |
var select = this.dialog.getElementById("filters"); | |
while (select.lastChild) { | |
select.removeChild(select.lastChild); | |
} | |
// For each element in the array, we extract out the display text and the | |
// value and put them into an option element to add to the select element. | |
for (var index=0; index<filters.length; ++index) { | |
var option = document.createElement('option'); | |
option.setAttribute("value", filters[index].value); | |
if (filters[index].value == viewFilter) { | |
option.setAttribute("selected", 'selected'); | |
} | |
var text = document.createTextNode(filters[index].display); | |
option.appendChild(text); | |
select.appendChild(option); | |
} | |
} | |
/** | |
* Given an entry, build an html UI element. | |
* @param entry An object representing a file entry. | |
* @param entry.URL The URL where this entry is located, if available, | |
* @param entry.thumbURL The URL of an image that represents a thumbnail | |
* preview of this file. | |
* @param entry.$type {String} The type of this entry. This can be one of: | |
* 'image', 'media', 'folder', 'document', 'html', or 'text' | |
* @param entry.name The name of this entry to display to the user. | |
*/ | |
PersistentStorage.prototype.buildThumbnail = function(entry) { | |
var self = this; | |
// We store the active module so that any elements built with data from this | |
// module can call back to it. | |
var module = this._backends[this._activeBackend].module; | |
var fileBlock = document.createElement('div'); | |
fileBlock.entry = entry; | |
fileBlock.className = 'file'; | |
// Use a closure here to make sure that the onclick handler has the | |
// right binding, | |
Xinha._addEvent(fileBlock, "click", function(ev) { | |
ev = ev || window.event; | |
Xinha._stopEvent(ev); | |
if (self.selectedEntry) { | |
// Unselect | |
removeClass(self.selectedEntry[1], 'selected'); | |
if (self.selectedEntry[1] == fileBlock) { | |
// Allow click to toggle. | |
self.selectedEntry = null; | |
return; | |
} | |
} | |
self.selectedEntry = [entry, fileBlock]; | |
self.selectedEntry[1].className += ' selected'; | |
var filename = self.dialog.getElementById('filename'); | |
filename.value = entry.name; | |
var confirmbutton = self.dialog.getElementById('confirm'); | |
confirmbutton.disabled = false; | |
}); | |
var image = document.createElement('img'); | |
image.className = 'icon'; | |
if (entry.$type == 'image') { | |
image.className = 'thumb'; | |
image.setAttribute('src', entry.URL); | |
} else if (entry.$type == 'folder') { | |
image.setAttribute('src', entry.thumbURL || entry.URL || this.editor.imgURL('images/tango/32x32/places/folder.png')); | |
} else if (entry.$type == 'document') { | |
image.setAttribute('src', entry.thumbURL || entry.URL || this.editor.imgURL('images/tango/32x32/mimetypes/text-html.png')); | |
} else if (entry.$type == 'html') { | |
image.setAttribute('src', entry.thumbURL || entry.URL || this.editor.imgURL('images/tango/32x32/mimetypes/text-html.png')); | |
} else if (entry.$type == 'txt') { | |
image.setAttribute('src', entry.thumbURL || entry.URL || this.editor.imgURL('images/tango/32x32/mimetypes/text-g-generic.png')); | |
} else { | |
image.setAttribute('src', entry.thumbURL || entry.URL || this.editor.imgURL('images/tango/32x32/mimetypes/x-office-document.png')); | |
} | |
image = fileBlock.appendChild(image); | |
var deleteIcon = document.createElement('img'); | |
deleteIcon.className = 'action delete'; | |
deleteIcon.setAttribute('alt', Xinha._lc('Delete', 'PersistentStorage')); | |
deleteIcon.setAttribute('title', Xinha._lc('Delete', 'PersistentStorage')); | |
deleteIcon.setAttribute('src', this.editor.imgURL('images/tango/16x16/places/user-trash.png')); | |
deleteIcon = fileBlock.appendChild(deleteIcon); | |
Xinha._addEvent(deleteIcon, "click", function(ev) { | |
ev = ev || window.event; | |
Xinha._stopEvent(ev); | |
module.deleteEntry(entry, function(success) { | |
module.cache = null; | |
if (success) { | |
fileBlock.parentNode.removeChild(fileBlock); | |
} else { | |
alert('Error deleting file ' + entry.name); | |
} | |
}); | |
}); | |
var fileName = document.createElement('p'); | |
fileName.className = 'filename'; | |
var fileNameText = document.createTextNode(entry.name); | |
fileNameText = fileName.appendChild(fileNameText); | |
fileName = fileBlock.appendChild(fileName); | |
// Use a closure here to make sure that the onclick handler has the | |
// right binding, | |
fileName.onclick = function(fileName) { | |
return function(ev) { | |
ev = ev || window.event; | |
Xinha._stopEvent(ev); | |
fileName.style.border = '1px solid red'; | |
return false; | |
}; | |
}(fileName); | |
var copyIcon = document.createElement('img'); | |
copyIcon.className = 'action copy'; | |
copyIcon.setAttribute('src', this.editor.imgURL('images/tango/16x16/actions/edit-copy.png')); | |
copyIcon.setAttribute('alt', Xinha._lc('Copy', 'PersistentStorage')); | |
copyIcon.setAttribute('title', Xinha._lc('Copy', 'PersistentStorage')); | |
Xinha._addEvent(copyIcon, "click", function(ev) { | |
ev = ev || window.event; | |
Xinha._stopEvent(ev); | |
module.copyEntry(entry, function(success, newentry) { | |
module.cache = null; | |
if (success) { | |
fileBlock.parentNode.appendChild(self.buildThumbnail(newentry)); | |
} else { | |
alert('Error copying file ' + entry.name); | |
} | |
}); | |
}); | |
copyIcon = fileBlock.appendChild(copyIcon); | |
return fileBlock; | |
} | |
/** | |
* Build up the file browser display in the element. | |
* @param metadata {Entry[]} A list of entries to display in the file browser. | |
* @param metadata[n].URL The URL where this entry is located, if available, | |
* @param metadata[n].thumbURL The URL of an image that represents a thumbnail | |
* preview of this file. | |
* @param metadata[n].$type {String} The type of this entry. This can be one of: | |
* 'image', 'media', 'folder', 'document', 'html', or 'text' | |
* @param metadata[n].name The name of this entry to display to the user. | |
*/ | |
PersistentStorage.prototype.displayBrowser = function(metadata) { | |
// The div element that contains the file browser. | |
var div = this.dialog.getElementById("fileList"); | |
// Clear out the previous directory listing. | |
while (div.lastChild) { | |
div.removeChild(div.lastChild); | |
} | |
var editor = this.editor; | |
var self = this; | |
for (var index=0; index<metadata.length; ++index) { | |
switch (this._viewType) { | |
case 'listing': | |
break; | |
case 'thumbnail': | |
default: | |
var thumb = div.appendChild(this.buildThumbnail(metadata[index])); | |
// If jQuery is loaded with the draggable and droppable UI controls, | |
// we'll enhance our interface. | |
if ($ && $.ui && $.ui.draggable && $.ui.droppable) { | |
// Make the objects draggable, but don't allow them to just hang | |
// around. | |
$(thumb).draggable({revert:true}); | |
// All folders become drop targets to allow for easier moving | |
// around. | |
if (metadata[index].$type == 'folder') { | |
// Just in case Xinha is used on a page with other draggables, we | |
// need to limit the targets we accept. | |
$(thumb).droppable({accept: '*', | |
drop: function(e, ui) { | |
// Set up a reference to the drop target so that we can move | |
// all dragged items into it. | |
var target = this; | |
ui.draggable.each(function() { | |
// We extract the entry reference from the DOM node. | |
var file = this; | |
var entry = file.entry; | |
// TODO: Whoagh... What if our backend is a drop target? | |
var module = self._backends[self._activeBackend].module; | |
// This only works intra-module... | |
module.moveEntry(entry, target.entry, function(success) { | |
module.cache = null; | |
if (success) { | |
file.parentNode.removeChild(file); | |
} else { | |
alert('Error deleting file ' + entry.name); | |
} | |
}); | |
}); | |
} | |
}); | |
} | |
} | |
} | |
} | |
// If the browser is empty, we'll display an empty message | |
if (!div.firstChild) { | |
var message = document.createTextNode('No files found'); | |
div.appendChild(message); | |
} | |
} | |
/** | |
* Update the file browser according to the latest criteria. | |
*/ | |
PersistentStorage.prototype.updateFileBrowser = function() { | |
var self = this; | |
var module = this._backends[this._activeBackend].module; | |
var buildDialog = function(cache) { | |
// We cache the module data to save server roundtrips. | |
module.cache = cache; | |
// The backends are expected to process their cached data to create | |
// two data structures for us. The first one is a list of filters | |
// used to keep the amount of data manageable in the interface. | |
// (e.g. directories, or tags, etc.) | |
var filters = module.getFilters(module.cache); | |
// Now that we have valid filter data, we setup the initial filter. | |
var resetFilter = true; | |
if (module.viewFilter) { | |
for (var index=0; index<filters.length; ++index) { | |
if (filters[index].value == module.viewFilter) { | |
resetFilter = false; | |
} | |
} | |
} | |
if (resetFilter) { | |
module.viewFilter = filters[0].value; | |
} | |
// The second data structure we need is a list of metadata about | |
// each file so that we can generate the appropriate listing. | |
var metadata = module.getMetadata(module.cache, module.viewFilter, self._typeFilter); | |
self.dialog.show(); | |
self.displayFilters(filters, module.viewFilter); | |
self.displayBrowser(metadata); | |
}; | |
if (module.cache) { | |
buildDialog(module.cache); | |
return; | |
} | |
module.loadData(buildDialog); | |
} | |
/** | |
* Prepare all external resources necessary to display the dialog. | |
*/ | |
PersistentStorage.prototype._prepareDialog = function() { | |
var self = this; | |
var editor = this.editor; | |
if (!this.html) { | |
Xinha._getback(Xinha.getPluginDir("PersistentStorage") + "/dialog.html", | |
function(getback) { | |
self.html = getback; | |
self._prepareDialog(); | |
}); | |
return; | |
} | |
this.dialog = new Xinha.Dialog(editor, this.html, "PersistentStorage", | |
{width: 800, | |
closeOnEscape: true, | |
resizable: true, | |
centered: true, | |
modal: true | |
}); | |
// The cancel dialog just hides the dialog. | |
this.dialog.getElementById("cancel").onclick = function() { | |
self.dialog.hide(); | |
}; | |
// Directory creation prompts the user for a directory name. | |
this.dialog.getElementById("dirCreate").onclick = function() { | |
var dirname = prompt(Xinha._lc('Please enter the name of the directory you\'d like to create.', 'PersistentStorage')); | |
if (dirname) { | |
var module = self._backends[self._activeBackend].module; | |
module.makeFolder(module.viewFilter, dirname, function(success) { | |
if (success) { | |
module.cache = null; | |
self.updateFileBrowser(); | |
} | |
}); | |
} | |
}; | |
// The import field is hidden by default, and clicking on it toggles that state. | |
var importLegend = this.dialog.getElementById("importlegend"); | |
var importDiv = this.dialog.getElementById("import"); | |
importLegend.onclick = function() { | |
toggleClass(importDiv, 'collapsed'); | |
}; | |
// Since the WWW icons is a static element, we hook up it's click handler | |
// here, rather than when showing the dialog | |
var iconWWW = this.dialog.getElementById("WWW"); | |
Xinha._addEvent(iconWWW, "click", function(ev) { | |
ev = ev || window.event; | |
Xinha._stopEvent(ev); | |
for (var indexEl=iconWWW.parentNode.firstChild; indexEl; indexEl = indexEl.nextSibling) { | |
if (1 != indexEl.nodeType) { | |
// We only process HTML nodes. | |
continue; | |
} | |
removeClass(indexEl, 'selected'); | |
} | |
addClass(iconWWW, 'selected'); | |
addClass(self.dialog.getElementById("placeBackend"), 'hidden'); | |
removeClass(self.dialog.getElementById("placeWww"), 'hidden'); | |
}); | |
// Clicking inside of the file browser will deselect all files. | |
var fileBrowser = this.dialog.getElementById("fileList"); | |
Xinha._addEvent(fileBrowser, "click", function(ev) { | |
for (var file=fileBrowser.firstChild; file; file=file.nextSibling) { | |
removeClass(file, 'selected'); | |
} | |
}); | |
// Setup the filter control so that user selection will refilter the shown | |
// files. | |
var filterControl = this.dialog.getElementById("filters"); | |
filterControl.onchange = function() { | |
var module = self._backends[self._activeBackend].module; | |
module.viewFilter = filterControl.options[filterControl.selectedIndex].value; | |
self.updateFileBrowser(); | |
}; | |
// Setup the viewtype control to change the internals of the file list. | |
var viewType = this.dialog.getElementById("viewType"); | |
viewType.onchange = function() { | |
self._viewType = viewType.options[viewType.selectedIndex].value; | |
self.updateFileBrowser(); | |
} | |
this.ready = true; | |
}; | |
/** | |
* Update the list of available backends. | |
*/ | |
PersistentStorage.prototype.updatePlacesDisplay = function(capability) { | |
var self = this; | |
var placesBrowser = this.dialog.getElementById("placesList"); | |
// Clear out any empty text nodes in the places browser. | |
while (placesBrowser.firstChild && 3 == placesBrowser.firstChild.nodeType) { | |
placesBrowser.removeChild(placesBrowser.firstChild); | |
} | |
var WWW = placesBrowser.firstChild; | |
while (WWW.nextSibling) { | |
placesBrowser.removeChild(WWW.nextSibling); | |
} | |
for (var backendName in this._backends) { | |
if (capability && !this._backends[backendName].config.capabilities[capability]) { | |
continue; | |
} | |
var thumb = placesBrowser.appendChild(this.placesIcon(this._backends[backendName])); | |
if ($ && $.ui && $.ui.draggable && $.ui.droppable) { | |
// Just in case Xinha is used on a page with other draggables, we | |
// need to limit the targets we accept. | |
$(thumb).droppable({accept: '*', | |
drop: function(e, ui) { | |
// Set up a reference to the drop target so that we can move | |
// all dragged items into it. | |
var target = this; | |
ui.draggable.each(function() { | |
// We extract the entry reference from the DOM node. | |
var file = this; | |
var entry = file.entry; | |
// TODO: Whoagh... What if our backend is a drop target? | |
var module = self._backends[self._activeBackend].module; | |
module.loadDocument(entry, function(documentSource) { | |
target.backend.module.saveDocument(module.viewFilter, entry.name, documentSource, function() { | |
target.backend.module.cache = null; | |
module.deleteEntry(entry, function(success) { | |
module.cache = null; | |
if (success) { | |
file.parentNode.removeChild(file); | |
} else { | |
alert('Error deleting file ' + entry.name); | |
} | |
}); | |
}); | |
}); | |
}); | |
} | |
}); | |
} | |
} | |
} | |
/** | |
* Set the currently active backend | |
*/ | |
PersistentStorage.prototype.setBackend = function(backendName) { | |
if (this._activeBackend == backendName) { | |
// Save ourselves some work. | |
return; | |
} | |
this._activeBackend = backendName; | |
var module = this._backends[this._activeBackend]; | |
if (module.config.capabilities.import_operations) { | |
Xinha._removeClass(this.dialog.getElementById("import"),'hidden'); | |
var importUI = this.dialog.getElementById("importui"); | |
while (importUI.firstChild) { | |
// Clear out the import UI of any other backend. | |
importUI.removeChild(importUI.firstChild); | |
} | |
module.module.buildImportUI(this.dialog, importUI); | |
} else { | |
Xinha._addClass(this.dialog.getElementById("import"),'hidden'); | |
} | |
} | |
/** | |
* Build a UI representation of the backend. | |
*/ | |
PersistentStorage.prototype.placesIcon = function(backend) { | |
var self = this; | |
var placesBlock = document.createElement('div'); | |
placesBlock.backend = backend; | |
placesBlock.className = 'file'; | |
if (this._activeBackend == backend.name) { | |
placesBlock.className += ' selected'; | |
} | |
// Use a closure here to make sure that the onclick handler has the | |
// right binding, | |
Xinha._addEvent(placesBlock, "click", function(ev) { | |
ev = ev || window.event; | |
Xinha._stopEvent(ev); | |
for (var indexEl=placesBlock.parentNode.firstChild; indexEl; indexEl = indexEl.nextSibling) { | |
if (1 != indexEl.nodeType) { | |
// We only process HTML nodes. | |
continue; | |
} | |
removeClass(indexEl, 'selected'); | |
} | |
addClass(placesBlock, 'selected'); | |
removeClass(self.dialog.getElementById("placeBackend"), 'hidden'); | |
addClass(self.dialog.getElementById("placeWww"), 'hidden'); | |
self.setBackend(backend.name); | |
self.updateFileBrowser(); | |
}); | |
var image = document.createElement('img'); | |
image.className = 'icon'; | |
image.setAttribute('src', backend.config.thumbURL || this.editor.imgURL('images/tango/32x32/places/network-server.png')); | |
image = placesBlock.appendChild(image); | |
var placesName = document.createElement('p'); | |
placesName.className = 'filename'; | |
var placesNameText = document.createTextNode(backend.config.displayName || backend.name); | |
placesNameText = placesName.appendChild(placesNameText); | |
placesName = placesBlock.appendChild(placesName); | |
return placesBlock; | |
} | |
/** | |
* The Xinha text editor | |
* @name Xinha | |
*/ | |
var Xinha = window.Xinha; | |
/** | |
* We override the native _insertImage function of Xinha. We call the instance | |
* version of the method in order to setup the proper context. | |
* @param image TODO Figure out what this is :-D | |
*/ | |
Xinha.prototype._insertImage = function(image) { | |
var editor = this; | |
var PS = editor.plugins['PersistentStorage'].instance; | |
// Setup the dialog title | |
var title = PS.dialog.getElementById("h1"); | |
title.innerHTML = Xinha._lc('Insert Image', 'PersistentStorage'); | |
// Setup the confirmation button and action. | |
var button = PS.dialog.getElementById("confirm"); | |
button.value = Xinha._lc('Insert', 'PersistentStorage'); | |
button.onclick = function() { | |
PS.insertImage(); | |
}; | |
PS.showDialog({typeFilter:['image','folder'],styleFor:'insertion'}); | |
} | |
/** | |
* Take a selected image from the dialog and insert it into the document. | |
*/ | |
PersistentStorage.prototype.insertImage = function() { | |
var editor = this.editor; | |
this.dialog.hide(); | |
var image = editor._doc.createElement('img'); | |
if (this.selectedEntry) { | |
image.setAttribute('src', this.selectedEntry[0].URL); | |
} else { | |
image.setAttribute('src', this.dialog.getElementById("URL").value); | |
} | |
for (var prop in {width:'',height:'',margin:'',padding:'',border:'',borderColor:'',backgroundColor:''}) { | |
var val = this.dialog.getElementById('image_' + prop).value.replace(/^\s+$/,''); | |
if (val) { | |
image.style[prop] = val; | |
} | |
} | |
editor.insertNodeAtSelection(image); | |
var range = editor.createRange(editor.getSelection()); | |
range.collapse(false); | |
} | |
})(window.jQuery); | |
//Xinha.prototype._linkFile = function(link) { | |
// | |
// var editor = this; | |
// var outparam = {"editor" : this, param : null}; | |
// if (typeof link == "undefined") { | |
// link = this.getParentElement(); | |
// if (link) { | |
// if (/^img$/i.test(link.tagName)) | |
// link = link.parentNode; | |
// if (!/^a$/i.test(link.tagName)) | |
// link = null; | |
// } | |
// } | |
// if (!link) { | |
// var sel = editor.getSelection(); | |
// var range = editor.createRange(sel); | |
// var compare = 0; | |
// if (Xinha.is_ie) { | |
// if ( sel.type == "Control" ) | |
// compare = range.length; | |
// else | |
// compare = range.compareEndPoints("StartToEnd", range); | |
// } else { | |
// compare = range.compareBoundaryPoints(range.START_TO_END, range); | |
// } | |
// if (compare == 0) { | |
// alert(Xinha._lc("You must select some text before making a new link.", 'PersistentStorage')); | |
// return; | |
// } | |
// outparam.param = { | |
// f_href : '', | |
// f_title : '', | |
// f_target : '', | |
// f_usetarget : editor.config.makeLinkShowsTarget, | |
// baseHref: editor.config.baseHref | |
// }; | |
// } else | |
// outparam.param = { | |
// f_href : Xinha.is_ie ? link.href : link.getAttribute("href"), | |
// f_title : link.title, | |
// f_target : link.target, | |
// f_usetarget : editor.config.makeLinkShowsTarget, | |
// baseHref: editor.config.baseHref | |
// }; | |
// | |
// Dialog(this.config.PersistentStorage.manager+'&mode=link', function(param){ | |
// if (!param) | |
// return false; | |
// var a = link; | |
// if (!a) try { | |
// editor._doc.execCommand("createlink", false, param.f_href); | |
// a = editor.getParentElement(); | |
// var sel = editor.getSelection(); | |
// var range = editor.createRange(sel); | |
// if (!Xinha.is_ie) { | |
// a = range.startContainer; | |
// if (!/^a$/i.test(a.tagName)) { | |
// a = a.nextSibling; | |
// if (a == null) | |
// a = range.startContainer.parentNode; | |
// } | |
// } | |
// } catch(e) {} | |
// else { | |
// var href = param.f_href.trim(); | |
// editor.selectNodeContents(a); | |
// if (href == "") { | |
// editor._doc.execCommand("unlink", false, null); | |
// editor.updateToolbar(); | |
// return false; | |
// } | |
// else { | |
// a.href = href; | |
// } | |
// } | |
// if (!(a && /^a$/i.test(a.tagName))) | |
// return false; | |
// a.target = param.f_target.trim(); | |
// a.title = param.f_title.trim(); | |
// editor.selectNodeContents(a); | |
// editor.updateToolbar(); | |
// }, outparam); | |
//}; | |
// | |
//function shortSize(cssSize) | |
//{ | |
// if(/ /.test(cssSize)) | |
// { | |
// var sizes = cssSize.split(' '); | |
// var useFirstSize = true; | |
// for(var i = 1; i < sizes.length; i++) | |
// { | |
// if(sizes[0] != sizes[i]) | |
// { | |
// useFirstSize = false; | |
// break; | |
// } | |
// } | |
// if(useFirstSize) cssSize = sizes[0]; | |
// } | |
// | |
// return cssSize; | |
//} | |
// | |
//function convertToHex(color) { | |
// | |
// if (typeof color == "string" && /, /.test.color) | |
// color = color.replace(/, /, ','); // rgb(a, b) => rgb(a,b) | |
// | |
// if (typeof color == "string" && / /.test.color) { // multiple values | |
// var colors = color.split(' '); | |
// var colorstring = ''; | |
// for (var i = 0; i < colors.length; i++) { | |
// colorstring += Xinha._colorToRgb(colors[i]); | |
// if (i + 1 < colors.length) | |
// colorstring += " "; | |
// } | |
// return colorstring; | |
// } | |
// | |
// return Xinha._colorToRgb(color); | |
//} | |
//}); | |
// Xinha._lc("Hello There A ") | |
// Xinha._lc("Hello \" There B \"") | |
// Xinha._lc('Hello There C ') | |
// Xinha._lc('Hello \' There D \'') | |