UNPKG

apostrophe

Version:

Apostrophe is a user-friendly content management system. You'll need more than this core module. See apostrophenow.org to get started.

387 lines (343 loc) • 12.6 kB
/* global XRegExp, rangy, $, _ */ /* global alert, prompt, AposMediaLibrary, AposTagEditor, aposPages */ if (!window.apos) { window.apos = {}; } $( document ).tooltip({ show: { effect: "fadeIn", duration: 200 }, hide: { effect: "fadeOut", duration: 200 }, position: { my: "left top+10", at: "left bottom" } }); var apos = window.apos; apos.widgetTypes = {}; var defaultWidgetTypes = [ 'slideshow', 'buttons', 'marquee', 'files', 'video', 'embed', 'pullquote', 'code', 'html' ]; // Add a widget type on the browser side. If typeName is `slideshow`, then Apostrophe invokes // the constructor function named `AposSlideshowWidgetEditor`, unless // `apos.data.widgetOptions.slideshow.constructorName` has been set to another name. // This means your server-side code can specify an alternate constructor by calling: // // apos.pushGlobalData({ // widgetOptions: { // slideshow: { // constructorName: 'MySlideshowEditor' // } // } // }) // // If you are developing your own widgets you should specify your own defaultConstructorName // so that you can avoid conflicts with future widgets in the Apos namespace. // // This happens after domready so that all javascript inline in the page, including // the calls that set up `apos.data`, can complete first. apos.addWidgetType = function(typeName, defaultConstructorName) { $(function() { var constructorName = (apos.data.widgetOptions && apos.data.widgetOptions[typeName] && apos.data.widgetOptions[typeName].constructorName) || defaultConstructorName || ('Apos' + apos.capitalizeFirst(typeName) + 'WidgetEditor'); var Constructor = window[constructorName]; try { apos.widgetTypes[typeName] = { label: Constructor.label, editor: Constructor }; // Allows properties known about the widget on the server side to be // seen on the browser side $.extend(true, apos.widgetTypes[typeName], (apos.data.widgetOptions && apos.data.widgetOptions[typeName]) || {}); } catch (e) { apos.log('Error constructing widget ' + typeName + ', expected constructor function named ' + constructorName + ', error was:'); throw e; } }); }; _.each(defaultWidgetTypes, function(type) { apos.addWidgetType(type); }); // KEEP IN SYNC WITH SERVER SIDE IMPLEMENTATION in search.js apos.slugify = window.sluggo; // Borrowed from the regexp-quote module for node apos.regExpQuote = function (string) { return string.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&"); }; // For use when storing DOM attributes meant to be compatible // with jQuery's built-in support for JSON parsing of // objects and arrays in data attributes. We'll be passing // the result to .attr, so we don't have to worry about // HTML escaping. apos.jsonAttribute = function(value) { if (typeof(value) === 'object') { return JSON.stringify(value); } else { return value; } }; // Given an array of items for an area, return true if the area // is considered empty. TODO: this is a lousy hack right now, // we don't have the capabilities we do on the server side to // identify empty blog widgets, etc. apos.areaIsEmpty = function(area) { return !_.find(area, function(item) { if ((area.type === 'richText') && (!area.content)) { return false; } return true; }); }; // Given an array of items for a singleton, return true if the // singleton is considered empty. TODO: right now this just // calls apos.areaIsEmpty, it should take the type into // account. apos.singletonIsEmpty = function(area, type) { return apos.areaIsEmpty(area); }; // Reusable utility to watch one jquery element's value and use it to // suggest valid slugs by updating the second jquery element's value. // If the initial slug contains slashes, only the last component // (after the last slash) is changed on title edits. If the original // slug (or its last component) is not in sync with the title, it is // assumed that the user already made deliberate changes to the slug, and // the slug is not automatically updated. apos.suggestSlugOnTitleEdits = function($title, $slug) { // Watch the title for changes, update the slug - but only if // the slug was in sync with the title to start with var originalTitle = $title.val(); var currentSlug = $slug.val(); var components = currentSlug.split('/'); var currentSlugTitle = components.pop(); if (currentSlugTitle === apos.slugify(originalTitle)) { $title.on('keyup', function() { var title = $title.val(); if (title !== originalTitle) { var currentSlug = $slug.val(); var components = currentSlug.split('/'); if (components.length) { components.pop(); var candidateSlugTitle = apos.slugify(title); components.push(candidateSlugTitle); var newSlug = components.join('/'); $slug.val(newSlug); } } }); } }; // Accept tags as a comma-separated string and sanitize them, // returning an array of zero or more nonempty strings. Must match // server side implementation apos.tagsToArray = function(tags) { if (typeof(tags) === 'number') { tags += ''; } if (typeof(tags) !== 'string') { return []; } tags += ''; tags = tags.split(/,\s*/); // split returns an array of one empty string for an empty source string ): tags = _.filter(tags, function(tag) { return tag.length > 0; }); // Make them all strings tags = _.map(tags, function(tag) { // Tags are always lowercase otherwise they will not compare // properly in MongoDB. If you want to change this then you'll // need to address that deeper issue return (tag + '').toLowerCase(); }); return tags; }; // Convert tags back to a string apos.tagsToString = function(s) { if (!$.isArray(s)) { // This is legit if no tags property exists yet on an object, don't bomb return ''; } var result = s.join(', '); return result; }; // Fix lame URLs. If we can't fix the URL, return null. // // Accepts valid URLs and relative URLs. If the URL smells like // it starts with a domain name, supplies an http:// prefix. // // KEEP IN SYNC WITH apostrophe.js SERVER SIDE VERSION apos.fixUrl = function(href) { if (href.match(/^(((https?|ftp)\:\/\/)|mailto\:|\#|([^\/\.]+)?\/|[^\/\.]+$)/)) { // All good - no change required return href; } else if (href.match(/^[^\/\.]+\.[^\/\.]+/)) { // Smells like a domain name. Educated guess: they left off http:// return 'http://' + href; } else { return null; } }; // Keep all older siblings of node, remove the rest (including node) from the DOM. apos.keepOlderSiblings = function(node) { while (node) { var next = node.nextSibling; node.parentNode.removeChild(node); node = next; } }; // Append all DOM sibling nodes after node, including // text nodes, to target. apos.moveYoungerSiblings = function(node, target) { var sibling = node.nextSibling; while (sibling) { var next = sibling.nextSibling; target.insertBefore(sibling, null); sibling = next; } }; // Enable autocomplete of tags. Expects the fieldset element // (not the input element) and an array of existing tags already // assigned to this item. // Options are passed in from addFields apos.enableTags = function($el, tags, field) { tags = tags || []; field = field || {}; if (apos.data.lockTags) { $el.find('[data-add]').remove(); } if (!field.limit) { field.limit = undefined; } if (!field.sortable) { field.sortable = undefined; } $el.selective({ preventDuplicates: true, add: !apos.data.lockTags, data: tags, source: '/apos/autocomplete-tag', addKeyCodes: [ 13, 'U+002C'], limit: field.limit, sortable: field.sortable, nestGuard: '[data-selective]' }); }; // Initialize a yes/no select element. If value is undefined (not just false), // def is used. apos.enableBoolean = function($el, value, def) { if (value === undefined) { value = def; } value = value ? '1' : '0'; $el.val(value); }; // Return the value of a yes/no select element as a proper // boolean (true or false). apos.getBoolean = function($el) { var val = $el.val(); // Quoted string '0' is considered truthy, fix that if (val === '0') { return false; } return !!val; }; // Set things up to instantiate the media library when the button is clicked. This is set up // to allow subclassing with an alternate constructor function apos.enableMediaLibrary = function() { $('body').on('click', '.apos-media-library-button', function() { if (!apos.data.mediaLibrary) { apos.data.mediaLibrary = {}; } var constructorName = apos.data.mediaLibrary.constructorName || 'AposMediaLibrary'; var mediaLibrary = new window[constructorName](apos.data.mediaLibrary); mediaLibrary.modal(); return false; }); }; // Set things up to instantiate the tag editor when the button is clicked. This is set up // to allow subclassing with an alternate constructor function apos.enableTagEditor = function() { $('body').on('click', '.apos-tag-editor-button', function() { if (!apos.data.tagEditorOptions) { apos.data.tagEditorOptions = {}; } var constructorName = apos.data.tagEditorOptions.constructorName || 'AposTagEditor'; var tagEditor = new window[constructorName](apos.tagEditorOptions); tagEditor.modal(); return false; }); }; // Simple progress display. Locates a progress display inside the given // element. If state is true, indicates activity, otherwise indicates // complete. Supports nested calls; does not revert to indicating complete // until the nesting level is 0. apos.busy = function($el, state) { var busy = $el.data('busy') || 0; if (state) { busy++; $el.data('busy', busy); if (busy === 1) { $el.find('[data-progress]').show(); $el.find('[data-finished]').hide(); } } else { busy--; $el.data('busy', busy); if (!busy) { $el.find('[data-progress]').hide(); $el.find('[data-finished]').show(); } } }; // Area editing is in apostrophe-editor-2, but // singleton editing still lives in the core apos.enableSingletons = function() { $('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 = { position: 'middle', size: 'full' }; var $item = $singleton.find('.apos-content .apos-widget:first'); if ($item.length) { itemData = apos.getWidgetData($item); } var editor = new apos.widgetTypes[type].editor({ data: itemData, save: function(callback) { if (slug) { // Has a slug, save it $.jsonCall('/apos/edit-singleton', { dataType: 'html', }, { slug: slug, options: JSON.parse($singleton.attr('data-options') || {}), // By now itemData has been updated (we passed it // into the widget and JavaScript passes objects by reference) content: itemData }, function(markup) { $singleton.find('.apos-content').html(markup); apos.enablePlayers($singleton); apos.emit('edited', $singleton); callback(null); }, function() { alert('Server error, please try again.'); callback('error'); }); } else { // Virtual singletons must be saved in other ways. Add it as a // data attribute of the singleton, and post an event $singleton.attr('data-item', JSON.stringify(itemData)); $singleton.trigger('aposEdited', itemData); return callback(); } }, // Options passed from the template or other environment options: $singleton.data('options') }); editor.init(); return false; }); $('body').on('click', '[data-page-versions]', function() { var slug = $(this).attr('data-page-versions'); aposPages.browseVersions({ slug: slug }); return false; }); }; apos.enableAreas = function() { apos.log('DEPRECATED: you do not have to call apos.enableAreas anymore. This stub is going away in 0.6.'); }; $(function() { // Do these late so that other code has a chance to override // None of these need to happen again on 'ready' events, so // just do them once apos.afterYield(function() { apos.enableMediaLibrary(); apos.enableTagEditor(); apos.enableSingletons(); }); });