apostrophe
Advanced tools
Comparing version 0.3.11 to 0.3.12
@@ -190,2 +190,23 @@ var oembed = require('oembed'); | ||
aposLocals.aposSingleton = function(options) { | ||
console.log('aposSingleton'); | ||
if (!self.itemTypes[options.type]) { | ||
console.log("Unknown item type: " + options.type); | ||
return; | ||
} | ||
// If someone transforms an existing area into a singleton, do a reasonable thing by | ||
// taking the first existing item of the proper type | ||
var item = _.find(options.area.items, function(item) { | ||
return item.type === options.type; | ||
}); | ||
if (!item) { | ||
item = { type: options.type }; | ||
} | ||
options.itemType = self.itemTypes[item.type]; | ||
options.item = item; | ||
console.log('Rendering!'); | ||
console.log(options); | ||
return partial('singleton.html', options); | ||
} | ||
aposLocals.aposAreaContent = function(items, options) { | ||
@@ -430,2 +451,41 @@ var result = ''; | ||
app.post('/apos/edit-singleton', function(req, res) { | ||
var slug = req.body.slug; | ||
self.permissions(req, 'edit-area', slug, function(err) { | ||
if (err) { | ||
return forbid(res); | ||
} | ||
var content = JSON.parse(req.body.content); | ||
// "OMG, what if they cheat and use a type not allowed for this singleton?" | ||
// When they refresh the page they will discover they can't see their hack. | ||
// aposSingleton only shows the first item of the specified type, regardless | ||
// of what is kicking around in the area. | ||
var type = content.type; | ||
var itemType = self.itemTypes[type]; | ||
if (!itemType) { | ||
return fail(req, res); | ||
} | ||
if (itemType.sanitize) { | ||
itemType.sanitize(content); | ||
} | ||
var area = { | ||
slug: req.body.slug, | ||
items: [ content ] | ||
}; | ||
self.putArea(slug, area, updated); | ||
function updated(err) { | ||
if (err) { | ||
console.log(err); | ||
return notfound(req, res); | ||
} | ||
return callLoadersForArea(area, function() { | ||
return res.send(aposLocals.aposAreaContent(area.items)); | ||
}); | ||
} | ||
}); | ||
}); | ||
// Used to render newly created, as yet unsaved widgets to be displayed in | ||
@@ -432,0 +492,0 @@ // the main apos editor. We're not really changing anything in the database |
{ | ||
"name": "apostrophe", | ||
"version": "0.3.11", | ||
"version": "0.3.12", | ||
"description": "Apostrophe is a user-friendly content management system. This core module of Apostrophe provides rich content editing and essential facilities to integrate Apostrophe into your Express project. Apostrophe also includes simple facilities for storing your rich content areas in MongoDB and fetching them back again. Additional functionality is available in modules like apostrophe-twitter and apostrophe-rss and forthcoming modules that address page trees, blog posts, events and the like.", | ||
@@ -5,0 +5,0 @@ "main": "apostrophe.js", |
@@ -583,4 +583,5 @@ if (!window.apos) { | ||
// When present in the context of a rich text editor, we interrogate | ||
// our placeholder div in that editor to get our current attributes | ||
if (options.widgetId) { | ||
self.exists = true; | ||
self.$widget = options.editor.$editable.find('.apos-widget[data-id="' + options.widgetId + '"]'); | ||
@@ -590,10 +591,25 @@ self.data = self.$widget.data(); | ||
self.widgetId = options.widgetId ? options.widgetId : apos.generateId(); | ||
self.data.id = self.widgetId; | ||
// When displayed as a singleton or an area that does not involve a | ||
// rich text editor as the larger context for all widgets, our data is passed in | ||
if (options.data) { | ||
self.data = options.data; | ||
apos.log(self.data); | ||
} | ||
// Make sure the selection we return to | ||
// is actually on the editor | ||
self.editor.$editable.focus(); | ||
// Make our own instance of the image editor template | ||
// so we don't have to fuss over old event handlers | ||
if (self.data.id) { | ||
self.exists = true; | ||
} | ||
if (!self.data.id) { | ||
self.data.id = apos.generateId(); | ||
} | ||
// Careful, relevant only when we are in a rich text editor context | ||
if (self.editor) { | ||
// Make sure the selection we return to | ||
// is actually on the editor | ||
self.editor.$editable.focus(); | ||
} | ||
// Make our own instance of the editor template | ||
self.$el = $(options.template + '.apos-template').clone(); | ||
@@ -625,3 +641,5 @@ self.$el.removeClass('.apos-template'); | ||
// Return focus to the main editor | ||
self.editor.$editable.focus(); | ||
if (self.editor) { | ||
self.editor.$editable.focus(); | ||
} | ||
}, | ||
@@ -665,2 +683,5 @@ | ||
createWidget: function() { | ||
if (!self.editor) { | ||
return; | ||
} | ||
self.$widget = $('<div></div>'); | ||
@@ -671,3 +692,2 @@ // self.$widget.attr('unselectable', 'on'); | ||
self.$widget.attr('data-type', self.type); | ||
self.$widget.attr('data-id', self.widgetId); | ||
}, | ||
@@ -681,2 +701,5 @@ | ||
updateWidget: function(callback) { | ||
if (!self.editor) { | ||
return callback(null); | ||
} | ||
var sizeAndPosition = self.getSizeAndPosition(); | ||
@@ -707,2 +730,5 @@ self.$widget.attr({ | ||
updateWidgetData: function() { | ||
if (!self.editor) { | ||
return; | ||
} | ||
_.each(self.data, function(val, key) { | ||
@@ -716,2 +742,5 @@ apos.log(key + ': ' + val); | ||
renderWidget: function(callback) { | ||
if (!self.editor) { | ||
return callback(null); | ||
} | ||
// Get all the data attributes | ||
@@ -742,2 +771,5 @@ var info = self.$widget.data(); | ||
insertWidget: function() { | ||
if (!self.editor) { | ||
return; | ||
} | ||
var markup = ''; | ||
@@ -820,3 +852,2 @@ | ||
self.preSave(function() { | ||
self.editor.undoPoint(); | ||
if (!self.exists) { | ||
@@ -826,6 +857,9 @@ alert(options.messages.missing); | ||
} | ||
var _new = false; | ||
if (!self.$widget) { | ||
self.createWidget(); | ||
_new = true; | ||
if (self.editor) { | ||
self.editor.undoPoint(); | ||
var _new = false; | ||
if (!self.$widget) { | ||
self.createWidget(); | ||
_new = true; | ||
} | ||
} | ||
@@ -840,3 +874,16 @@ self.updateWidget(function(err) { | ||
// self.editor.updateWidgetBackup(self.widgetId, self.$widget); | ||
self.destroy(); | ||
if (options.save) { | ||
// Used to implement save for singletons and non-rich-text-based areas. | ||
// Note that in this case options.data was passed in by reference, | ||
// so the end result can be read there. Pay attention to the callback so | ||
// we can allow the user a second chance | ||
options.save(function(err) { | ||
if (!err) { | ||
self.destroy(); | ||
} | ||
}); | ||
} else { | ||
self.destroy(); | ||
} | ||
}); | ||
@@ -861,4 +908,4 @@ }); | ||
if (self.exists) { | ||
sizeAndPosition.size = self.$widget.attr('data-size'); | ||
sizeAndPosition.position = self.$widget.attr('data-position'); | ||
sizeAndPosition.size = self.data.size; | ||
sizeAndPosition.position = self.data.position; | ||
} | ||
@@ -890,6 +937,6 @@ self.$el.find('input[name="size"]').prop('checked', false); | ||
self.afterCreatingEl = function() { | ||
self.$el.find('[data-iframe-placeholder]').replaceWith($('<iframe id="iframe-' + self.widgetId + '" name="iframe-' + self.widgetId + '" class="apos-file-iframe" src="/apos/file-iframe/' + self.widgetId + '"></iframe>')); | ||
self.$el.find('[data-iframe-placeholder]').replaceWith($('<iframe id="iframe-' + self.data.id + '" name="iframe-' + self.data.id + '" class="apos-file-iframe" src="/apos/file-iframe/' + self.data.id + '"></iframe>')); | ||
self.$el.bind('uploaded', function(e, id) { | ||
// Only react to events intended for us | ||
if (id === self.widgetId) { | ||
if (id === self.data.id) { | ||
self.exists = true; | ||
@@ -906,3 +953,3 @@ self.preview(); | ||
if (self.exists) { | ||
$.getJSON('/apos/file-info/' + self.widgetId, function(info) { | ||
$.getJSON('/apos/file-info/' + self.data.id, function(info) { | ||
self.data.extension = info.extension; | ||
@@ -1206,8 +1253,8 @@ callback(); | ||
apos.enableAreas = function() { | ||
$('.apos-edit-area').click(function() { | ||
$('body').on('click', '.apos-edit-area', function() { | ||
var area = $(this).closest('.apos-area'); | ||
var slug = area.attr('data-apos-slug'); | ||
var slug = area.attr('data-slug'); | ||
$.get('/apos/edit-area', { slug: slug, controls: area.attr('data-apos-controls') }, function(data) { | ||
$.get('/apos/edit-area', { slug: slug, controls: area.attr('data-controls') }, function(data) { | ||
area.find('.apos-edit-view').remove(); | ||
@@ -1225,3 +1272,3 @@ var editView = $('<div class="apos-edit-view"></div>'); | ||
area.find('[data-save-area]').click(function() { | ||
var slug = area.attr('data-apos-slug'); | ||
var slug = area.attr('data-slug'); | ||
$.post('/apos/edit-area', | ||
@@ -1251,2 +1298,37 @@ { | ||
}); | ||
$('body').on('click', '.apos-edit-singleton', function() { | ||
var $singleton = $(this).closest('.apos-singleton'); | ||
var slug = $singleton.attr('data-slug'); | ||
var type = $singleton.attr('data-type'); | ||
var itemData = {}; | ||
var $item = $singleton.find('.apos-content .apos-widget :first'); | ||
if ($item.length) { | ||
itemData = $item.data(); | ||
} | ||
new apos.widgetTypes[type].editor({ | ||
data: itemData, | ||
save: function(callback) { | ||
apos.log(itemData); | ||
$.post('/apos/edit-singleton', | ||
{ | ||
slug: slug, | ||
// By now itemData has been updated (we passed it | ||
// into the widget and JavaScript passes objects by reference) | ||
content: JSON.stringify(itemData) | ||
}, | ||
function(markup) { | ||
$singleton.find('.apos-content').html(markup); | ||
apos.enablePlayers($singleton); | ||
callback(null); | ||
} | ||
).fail(function() { | ||
alert('Server error, please try again.'); | ||
callback('error'); | ||
}); | ||
} | ||
}); | ||
return false; | ||
}); | ||
}; | ||
@@ -1253,0 +1335,0 @@ |
132
README.md
# Apostrophe | ||
## This is an early alpha quality version of Apostrophe 2, for node.js developers. Most frontend design work has not happened yet. Page trees, blogs events, etc. are not part of Apostrophe 2 yet. See [Apostrophe 1.5](http://apostrophenow.org) for the current stable and mature release of Apostrophe for PHP and Symfony. | ||
## This is an early alpha quality version of Apostrophe 2, for node.js developers. Most frontend design work has not happened yet. Blogs, events, etc. are not part of Apostrophe 2 yet. See [Apostrophe 1.5](http://apostrophenow.org) for the current stable and mature release of Apostrophe for PHP and Symfony. | ||
@@ -9,3 +9,3 @@ Apostrophe is a content management system. This core module provides rich content editing as well as essential services to tie Apostrophe to your Express application. | ||
[You can try a live demo of the Apostrophe 2 Wiki sample app here.](http://demowiki.apostrophenow.com/) (Note: the demo site resets at the top of the hour.) See also the [apostrophe-wiki github project](http://github.com/punkave/apostrophe-wiki). | ||
[You can try a live demo of the Apostrophe 2 sandbox app here.](http://demo2.apostrophenow.com/) (Note: the demo site resets at the top of the hour.) See also the [apostrophe-sandbox github project](http://github.com/punkave/apostrophe-sandbox). | ||
@@ -41,3 +41,3 @@ Apostrophe introduces "widgets," separate editors for rich media items like photos, videos, pullquotes and code samples. Apostrophe's widgets handle these items much better than a rich text editor on its own. | ||
node, of course | ||
mongodb, on your local machine (or edit wiki.js to point somewhere else) | ||
mongodb, on your local machine (or edit app.js to point somewhere else) | ||
imagemagick, to resize uploaded images (specifically the `convert` command line tool) | ||
@@ -53,24 +53,37 @@ | ||
Here's the `initApos` function of the sample application [http://github.com/punkave/aposwiki](aposwiki). Notice this function invokes a callback when it's done. `wiki.js` makes good use of the `async` module to carry out its initialization tasks elegantly. | ||
Here's the `initApos` function of the sample application [http://github.com/punkave/apostrophe-sandbox](apostrophe-sandbox). Notice this function invokes a callback when it's done. `app.js` makes good use of the `async` module to carry out its initialization tasks elegantly. Here we also initialize other modules that snap into Apostrophe: | ||
function initApos(callback) { | ||
return apos.init({ | ||
files: appy.files, | ||
areas: appy.areas, | ||
pages: appy.pages, | ||
app: app, | ||
uploadfs: uploadfs, | ||
permissions: aposPermissions, | ||
}, callback); | ||
require('apostrophe-twitter')({ apos: apos, app: app }); | ||
require('apostrophe-rss')({ apos: apos, app: app }); | ||
async.series([initAposMain, initAposPages], callback); | ||
function initAposMain(callback) { | ||
console.log('initAposMain'); | ||
return apos.init({ | ||
db: db, | ||
app: app, | ||
uploadfs: uploadfs, | ||
permissions: aposPermissions, | ||
// Allows us to extend shared layouts | ||
partialPaths: [ __dirname + '/views/global' ] | ||
}, callback); | ||
} | ||
function initAposPages(callback) { | ||
console.log('initAposPages'); | ||
pages = require('apostrophe-pages')({ apos: apos, app: app }, callback); | ||
} | ||
} | ||
"What are `appy.files`, `appy.areas` and `appy.pages`?" MongoDB collections. You are responsible for connecting to MongoDB and creating these three collection objects, then providing them to Apostrophe. (Hint: it's pretty convenient with Appy.) For best results, `areas` and `pages` should both have a unique index on the `slug` property. The `areas` collection is used for independent areas, while the `pages` collection is handy when areas should be logically grouped together (consider the "main" and "sidebar" content areas of a webpage, for instance). Apostrophe's getArea, putArea and getPage methods make all that easy for you as you'll see below. | ||
"Where does db come from?" It's a MongoDB native database connection. (Hint: convenient to set up with Appy, or just use mongodb-native yourself.) Apostrophe's getArea, putArea and getPage methods utilize these. | ||
"What is `app`?" `app` is your Express 3.0 app object. See the Express documentation for how to create an application. Again, Appy helps here. | ||
"What is `uploadfs`?" [http://github.com/punkave/uploadfs](uploadfs) is a module that conveniently stores uploaded files in either the local filesystem or S3, whichever you like. See `wiki.js` for an example of configuration. You'll create an `uploadfs` instance, initialize it and then pass it in here. | ||
"What is `uploadfs`?" [http://github.com/punkave/uploadfs](uploadfs) is a module that conveniently stores uploaded files in either the local filesystem or S3, whichever you like. See `app.js` in the `apostrophe-sandbox` project for an example of configuration. You'll create an `uploadfs` instance, initialize it and then pass it in here. | ||
"What is `aposPermissions`?" A function you define to decide who is allowed to edit content. If you skip this parameter, Apostrophe allows everyone to edit everything - not safe in production of course, but convenient in the early development stages. | ||
To understand configuration in detail, you should really check out `wiki.js`. Please don't suffer without reading that simple and well-commented example. | ||
To understand configuration in detail, you should really check out `app.js`. Please don't suffer without reading that simple and well-commented example. | ||
@@ -114,3 +127,3 @@ ### Making Sure Apostrophe Is In The Browser | ||
The easiest way to add Apostrophe-powered editable rich content areas to your Node Express 3.0 project is to use Apostrophe's `aposArea` function, which is made available to your Express templates when you configure Apostrophe. Here's a simple example taken from the aposwiki sample application: | ||
The easiest way to add Apostrophe-powered editable rich content areas to your Node Express 3.0 project is to use Apostrophe's `aposArea` function, which is made available to your Express templates when you configure Apostrophe. Here's a simple example taken from the apostrophe-sandbox sample application: | ||
@@ -131,3 +144,3 @@ {{ aposArea({ slug: 'main', items: main, edit: true }) }} | ||
"Where does `items` come from?" Good question. You are responsible for fetching the content as part of the Express route code that renders your template. You do this with Apostrophe's `getArea` and `getPage` methods. | ||
"Where does `items` come from?" Good question. You are responsible for fetching the content as part of the Express route code that renders your template. You do this with Apostrophe's `getArea` and `getPage` methods. [Note: if you just want a tree of editable pages, use the apostrophe-pages module to do most of this work.](http://github.com/punkave/apostrophe-pages) | ||
@@ -144,4 +157,16 @@ Naturally `getArea` is asynchronous: | ||
Also note that there is an `err` parameter to the callback. Real-world applications should check for errors (and the `wiki.js` sample application does). | ||
Also note that there is an `err` parameter to the callback. Real-world applications should check for errors (and the `app.js` sample application does). | ||
## Displaying Single Widgets ("Singletons") | ||
Of course, sometimes you want to enforce a more specific design for an editable page. You might, for instance, want to require the user to pick a video for the upper right corner. You can do that with `aposSingleton`: | ||
{{ aposSingleton({ slug: slug + ':sidebarVideo', type: 'video', area: page.areas.sidebarVideo, edit: edit }) }} | ||
Note that singletons are stored as areas. The only difference is that the interface only displays and edits the first item of the specified type found in the area. There is no rich text editor "wrapped around" the widget, so clicking "edit" for a video immediately displays the video dialog box. | ||
Only widgets (images, videos and the like) may be specified as types for singletons. For a standalone rich-text editor that doesn't allow any widgets, just limit the set of controls to those that are not widgets: | ||
{{ aposArea({ slug: 'main', items: main, edit: true, controls: [ 'style', 'bold', 'italic', 'createLink' ] }) }} | ||
## Grouping Areas Into "Pages" | ||
@@ -183,61 +208,4 @@ | ||
The [apostrophe-wiki sample application](http://github.com/punkave/apostrophe-wiki) uses this method to deliver complete Wiki pages: | ||
The [apostrophe-pages module](http://github.com/punkave/apostrophe-pages) uses this method to deliver complete pages automatically for you. In most cases this is what you'll want to do. In rarer cases you'll write your own routes that need to deliver content. See the sandbox project and the `apostrophe-pages` module for examples. | ||
app.get('*', function(req, res) { | ||
var slug = req.params[0]; | ||
apos.getPage(slug, function(e, info) { | ||
return res.render('page.html', { | ||
slug: info.slug, | ||
main: info.areas.main ? info.areas.main.items : [], | ||
sidebar: info.areas.sidebar ? info.areas.sidebar.items : [] | ||
}); | ||
}); | ||
}); | ||
This is a simplified example. The actual code in `wiki.js` uses middleware to summon the page and footer information into the `req` object. There are two middleware functions, one for the page contents and one for a globally shared footer. It's a good strategy to consider. Perhaps we'll add some standard middleware functions like this soon: | ||
app.get('*', | ||
function(req, res, next) { | ||
// Get content for this page | ||
req.slug = req.params[0]; | ||
apos.getPage(req.slug, function(e, info) { | ||
if (e) { | ||
console.log(e); | ||
return fail(req, res); | ||
} | ||
if (!info) { | ||
info = { slug: slug, areas: {} }; | ||
} | ||
req.page = info; | ||
return next(); | ||
}); | ||
}, | ||
function(req, res, next) { | ||
// Get the shared footer | ||
apos.getArea('footer', function(e, info) { | ||
if (e) { | ||
console.log(e); | ||
return fail(req, res); | ||
} | ||
req.footer = info; | ||
return next(); | ||
}); | ||
}, | ||
function (req, res) { | ||
return res.render('page.html', { | ||
slug: req.slug, | ||
main: req.page.areas.main ? req.page.areas.main.items : [], | ||
sidebar: req.page.areas.sidebar ? req.page.areas.sidebar.items : [], | ||
user: req.user, | ||
edit: req.user && req.user.username === 'admin', | ||
footer: req.footer ? req.footer.content : '' | ||
}); | ||
} | ||
); | ||
Now `page.jade` can call `aposArea` to render the areas: | ||
{{ aposArea({ slug: slug + ':main', items: main, edit: true }) }} | ||
{{ aposArea({ slug: slug + ':sidebar', items: sidebar, edit: true }) }} | ||
## Enforcing Permissions | ||
@@ -255,8 +223,8 @@ | ||
Currently the possible actions are `edit-area` and `edit-media`. `edit-area` calls will include the slug of the area as the third parameter. `edit-media` calls for existing files may include a `file` object retrieved from Apostrophe's database, with an "owner" property set to the _id, id or username property of `req.user` at the time the file was last edited. `edit-media` calls with no existing file parameter also occur, for new file uploads. | ||
Currently the possible actions are `edit-area`, `edit-media`, `edit-page` and `view-page` (the latter two are added by the `apostrophe-pages` module). `edit-area` calls will include the slug of the area as the third parameter. `edit-media` calls for existing files may include a `file` object retrieved from Apostrophe's database, with an "owner" property set to the _id, id or username property of `req.user` at the time the file was last edited. `edit-media` calls with no existing file parameter also occur, for new file uploads. | ||
A common case is to restrict editing to a single user: | ||
A common case is to restrict editing to a single user but let view actions sail through: | ||
function permissions(req, action, fileOrSlug, callback) { | ||
if (req.user && (req.user.username === 'admin')) { | ||
if (req.user && (!action.match(/^view-/)) && (req.user.username === 'admin')) { | ||
// OK | ||
@@ -269,3 +237,3 @@ return callback(null); | ||
You can see an example of this pattern in `wiki.js`. | ||
You can see an example of this pattern in `app.js` in the sandbox project. | ||
@@ -276,2 +244,4 @@ ## Extending Apostrophe | ||
The [apostrophe-pages](http://github.com/punkave/apostrophe-pages) module extends Apostrophe with full blown support for trees of editable web pages (like having a static site, except of course that your users can edit it gorgeously and intuitively). | ||
## Roadmap | ||
@@ -288,4 +258,2 @@ | ||
* It should be possible to fetch just certain rich media from areas conveniently and quickly (technically possible now, see above). | ||
* Server-side renders should be cached, for a minimum lifetime equal to that of the widget with the shortest cache lifetime. | ||
* A separate module that complements Apostrophe by managing "pages" in a traditional page tree should be developed. | ||
@@ -292,0 +260,0 @@ ## Conclusion and Contact Information |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
2837029
135
14855
266