Remove files after rebase

This commit is contained in:
David Craig 2016-01-21 12:08:06 +13:00 committed by Damian Mooyman
parent 2140025c20
commit 2fc9d69197
30 changed files with 0 additions and 7680 deletions

View File

@ -1,396 +0,0 @@
/**
* File: LeftAndMain.BatchActions.js
*/
(function($) {
$.entwine('ss.tree', function($){
/**
* Class: #Form_BatchActionsForm
*
* Batch actions which take a bunch of selected pages,
* usually from the CMS tree implementation, and perform serverside
* callbacks on the whole set. We make the tree selectable when the jQuery.UI tab
* enclosing this form is opened.
*
* Events:
* register - Called before an action is added.
* unregister - Called before an action is removed.
*/
$('#Form_BatchActionsForm').entwine({
/**
* Variable: Actions
* (Array) Stores all actions that can be performed on the collected IDs as
* function closures. This might trigger filtering of the selected IDs,
* a confirmation message, etc.
*/
Actions: [],
getTree: function() {
return $('.cms-tree');
},
fromTree: {
oncheck_node: function(e, data){
this.serializeFromTree();
},
onuncheck_node: function(e, data){
this.serializeFromTree();
}
},
/**
* @func registerDefault
* @desc Register default bulk confirmation dialogs
*/
registerDefault: function() {
// Publish selected pages action
this.register('admin/pages/batchactions/publish', function(ids) {
var confirmed = confirm(
ss.i18n.inject(
ss.i18n._t(
"CMSMAIN.BATCH_PUBLISH_PROMPT",
"You have {num} page(s) selected.\n\nDo you really want to publish?"
),
{'num': ids.length}
)
);
return (confirmed) ? ids : false;
});
// Unpublish selected pages action
this.register('admin/pages/batchactions/unpublish', function(ids) {
var confirmed = confirm(
ss.i18n.inject(
ss.i18n._t(
"CMSMAIN.BATCH_UNPUBLISH_PROMPT",
"You have {num} page(s) selected.\n\nDo you really want to unpublish"
),
{'num': ids.length}
)
);
return (confirmed) ? ids : false;
});
// Delete selected pages action
// @deprecated since 4.0 Use archive instead
this.register('admin/pages/batchactions/delete', function(ids) {
var confirmed = confirm(
ss.i18n.inject(
ss.i18n._t(
"CMSMAIN.BATCH_DELETE_PROMPT",
"You have {num} page(s) selected.\n\nDo you really want to delete?"
),
{'num': ids.length}
)
);
return (confirmed) ? ids : false;
});
// Delete selected pages action
this.register('admin/pages/batchactions/archive', function(ids) {
var confirmed = confirm(
ss.i18n.inject(
ss.i18n._t(
"CMSMAIN.BATCH_ARCHIVE_PROMPT",
"You have {num} page(s) selected.\n\nAre you sure you want to archive these pages?\n\nThese pages and all of their children pages will be unpublished and sent to the archive."
),
{'num': ids.length}
)
);
return (confirmed) ? ids : false;
});
// Restore selected archived pages
this.register('admin/pages/batchactions/restore', function(ids) {
var confirmed = confirm(
ss.i18n.inject(
ss.i18n._t(
"CMSMAIN.BATCH_RESTORE_PROMPT",
"You have {num} page(s) selected.\n\nDo you really want to restore to stage?\n\nChildren of archived pages will be restored to the root level, unless those pages are also being restored."
),
{'num': ids.length}
)
);
return (confirmed) ? ids : false;
});
// Delete selected pages from live action
this.register('admin/pages/batchactions/deletefromlive', function(ids) {
var confirmed = confirm(
ss.i18n.inject(
ss.i18n._t(
"CMSMAIN.BATCH_DELETELIVE_PROMPT",
"You have {num} page(s) selected.\n\nDo you really want to delete these pages from live?"
),
{'num': ids.length}
)
);
return (confirmed) ? ids : false;
});
},
onadd: function() {
this.registerDefault();
this._super();
},
/**
* @func register
* @param {string} type
* @param {function} callback
*/
register: function(type, callback) {
this.trigger('register', {type: type, callback: callback});
var actions = this.getActions();
actions[type] = callback;
this.setActions(actions);
},
/**
* @func unregister
* @param {string} type
* @desc Remove an existing action.
*/
unregister: function(type) {
this.trigger('unregister', {type: type});
var actions = this.getActions();
if(actions[type]) delete actions[type];
this.setActions(actions);
},
/**
* @func refreshSelected
* @param {object} rootNode
* @desc Ajax callbacks determine which pages is selectable in a certain batch action.
*/
refreshSelected : function(rootNode) {
var self = this,
st = this.getTree(),
ids = this.getIDs(),
allIds = [],
viewMode = $('.cms-content-batchactions-button'),
actionUrl = this.find(':input[name=Action]').val();
// Default to refreshing the entire tree
if(rootNode == null) rootNode = st;
for(var idx in ids) {
$($(st).getNodeByID(idx)).addClass('selected').attr('selected', 'selected');
}
// If no action is selected, enable all nodes
if(!actionUrl || actionUrl == -1 || !viewMode.hasClass('active')) {
$(rootNode).find('li').each(function() {
$(this).setEnabled(true);
});
return;
}
// Disable the nodes while the ajax request is being processed
$(rootNode).find('li').each(function() {
allIds.push($(this).data('id'));
$(this).addClass('treeloading').setEnabled(false);
});
// Post to the server to ask which pages can have this batch action applied
// Retain existing query parameters in URL before appending path
var actionUrlParts = $.path.parseUrl(actionUrl);
var applicablePagesUrl = actionUrlParts.hrefNoSearch + '/applicablepages/';
applicablePagesUrl = $.path.addSearchParams(applicablePagesUrl, actionUrlParts.search);
applicablePagesUrl = $.path.addSearchParams(applicablePagesUrl, {csvIDs: allIds.join(',')});
jQuery.getJSON(applicablePagesUrl, function(applicableIDs) {
// Set a CSS class on each tree node indicating which can be batch-actioned and which can't
jQuery(rootNode).find('li').each(function() {
$(this).removeClass('treeloading');
var id = $(this).data('id');
if(id == 0 || $.inArray(id, applicableIDs) >= 0) {
$(this).setEnabled(true);
} else {
// De-select the node if it's non-applicable
$(this).removeClass('selected').setEnabled(false);
$(this).prop('selected', false);
}
});
self.serializeFromTree();
});
},
/**
* @func serializeFromTree
* @return {boolean}
*/
serializeFromTree: function() {
var tree = this.getTree(), ids = tree.getSelectedIDs();
// write IDs to the hidden field
this.setIDs(ids);
return true;
},
/**
* @func setIDS
* @param {array} ids
*/
setIDs: function(ids) {
this.find(':input[name=csvIDs]').val(ids ? ids.join(',') : null);
},
/**
* @func getIDS
* @return {array}
*/
getIDs: function() {
// Map empty value to empty array
var value = this.find(':input[name=csvIDs]').val();
return value
? value.split(',')
: [];
},
onsubmit: function(e) {
var self = this, ids = this.getIDs(), tree = this.getTree(), actions = this.getActions();
// if no nodes are selected, return with an error
if(!ids || !ids.length) {
alert(ss.i18n._t('CMSMAIN.SELECTONEPAGE', 'Please select at least one page'));
e.preventDefault();
return false;
}
// apply callback, which might modify the IDs
var type = this.find(':input[name=Action]').val();
if(actions[type]) {
ids = this.getActions()[type].apply(this, [ids]);
}
// Discontinue processing if there are no further items
if(!ids || !ids.length) {
e.preventDefault();
return false;
}
// write (possibly modified) IDs back into to the hidden field
this.setIDs(ids);
// Reset failure states
tree.find('li').removeClass('failed');
var button = this.find(':submit:first');
button.addClass('loading');
jQuery.ajax({
// don't use original form url
url: type,
type: 'POST',
data: this.serializeArray(),
complete: function(xmlhttp, status) {
button.removeClass('loading');
// Refresh the tree.
// Makes sure all nodes have the correct CSS classes applied.
tree.jstree('refresh', -1);
self.setIDs([]);
// Reset action
self.find(':input[name=Action]').val('').change();
// status message (decode into UTF-8, HTTP headers don't allow multibyte)
var msg = xmlhttp.getResponseHeader('X-Status');
if(msg) statusMessage(decodeURIComponent(msg), (status == 'success') ? 'good' : 'bad');
},
success: function(data, status) {
var id, node;
if(data.modified) {
var modifiedNodes = [];
for(id in data.modified) {
node = tree.getNodeByID(id);
tree.jstree('set_text', node, data.modified[id]['TreeTitle']);
modifiedNodes.push(node);
}
$(modifiedNodes).effect('highlight');
}
if(data.deleted) {
for(id in data.deleted) {
node = tree.getNodeByID(id);
if(node.length) tree.jstree('delete_node', node);
}
}
if(data.error) {
for(id in data.error) {
node = tree.getNodeByID(id);
$(node).addClass('failed');
}
}
},
dataType: 'json'
});
// Never process this action; Only invoke via ajax
e.preventDefault();
return false;
}
});
$('.cms-content-batchactions-button').entwine({
onmatch: function () {
this._super();
this.updateTree();
},
onunmatch: function () {
this._super();
},
onclick: function (e) {
this.updateTree();
},
updateTree: function () {
var tree = $('.cms-tree'),
form = $('#Form_BatchActionsForm');
this._super();
if(this.data('active')) {
tree.addClass('multiple');
tree.removeClass('draggable');
form.serializeFromTree();
} else {
tree.removeClass('multiple');
tree.addClass('draggable');
}
$('#Form_BatchActionsForm').refreshSelected();
}
});
/**
* Class: #Form_BatchActionsForm :select[name=Action]
*/
$('#Form_BatchActionsForm select[name=Action]').entwine({
onchange: function(e) {
var form = $(e.target.form),
btn = form.find(':submit'),
selected = $(e.target).val();
if(!selected || selected == -1) {
btn.attr('disabled', 'disabled').button('refresh');
} else {
btn.removeAttr('disabled').button('refresh');
}
// Refresh selected / enabled nodes
$('#Form_BatchActionsForm').refreshSelected();
// TODO Should work by triggering change() along, but doesn't - entwine event bubbling?
this.trigger("liszt:updated");
this._super(e);
}
});
});
})(jQuery);

View File

@ -1,99 +0,0 @@
(function($) {
$.entwine('ss', function($){
/**
* The "content" area contains all of the section specific UI (excluding the menu).
* This area can be a form itself, as well as contain one or more forms.
* For example, a page edit form might fill the whole area,
* while a ModelAdmin layout shows a search form on the left, and edit form on the right.
*/
$('.cms-content').entwine({
onadd: function() {
var self = this;
// Force initialization of certain UI elements to avoid layout glitches
this.find('.cms-tabset').redrawTabs();
this._super();
},
redraw: function() {
if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
// Force initialization of certain UI elements to avoid layout glitches
this.add(this.find('.cms-tabset')).redrawTabs();
this.find('.cms-content-header').redraw();
this.find('.cms-content-actions').redraw();
}
});
/**
* Load edit form for the selected node when its clicked.
*/
$('.cms-content .cms-tree').entwine({
onadd: function() {
var self = this;
this._super();
this.bind('select_node.jstree', function(e, data) {
var node = data.rslt.obj, loadedNodeID = self.find(':input[name=ID]').val(), origEvent = data.args[2], container = $('.cms-container');
// Don't trigger unless coming from a click event.
// Avoids problems with automated section switches from tree to detail view
// when JSTree auto-selects elements on first load.
if(!origEvent) {
return false;
}
// Don't allow checking disabled nodes
if($(node).hasClass('disabled')) return false;
// Don't allow reloading of currently selected node,
// mainly to avoid doing an ajax request on initial page load
if($(node).data('id') == loadedNodeID) return;
var url = $(node).find('a:first').attr('href');
if(url && url != '#') {
// strip possible querystrings from the url to avoid duplicateing document.location.search
url = url.split('?')[0];
// Deselect all nodes (will be reselected after load according to form state)
self.jstree('deselect_all');
self.jstree('uncheck_all');
// Ensure URL is absolute (important for IE)
if($.path.isExternal($(node).find('a:first'))) url = url = $.path.makeUrlAbsolute(url, $('base').attr('href'));
// Retain search parameters
if(document.location.search) url = $.path.addSearchParams(url, document.location.search.replace(/^\?/, ''));
// Load new page
container.loadPanel(url);
} else {
self.removeForm();
}
});
}
});
$('.cms-content .cms-content-fields').entwine({
redraw: function() {
if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
}
});
$('.cms-content .cms-content-header, .cms-content .cms-content-actions').entwine({
redraw: function() {
if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
// Fix dimensions to actual extents, in preparation for a relayout via jslayout.
this.height('auto');
this.height(this.innerHeight()-this.css('padding-top')-this.css('padding-bottom'));
}
});
});
})(jQuery);

View File

@ -1,418 +0,0 @@
/**
* File: LeftAndMain.EditForm.js
*/
(function($) {
// Can't bind this through jQuery
window.onbeforeunload = function(e) {
var form = $('.cms-edit-form');
form.trigger('beforesubmitform');
if(form.is('.changed')) return ss.i18n._t('LeftAndMain.CONFIRMUNSAVEDSHORT');
};
$.entwine('ss', function($){
/**
* Class: .cms-edit-form
*
* Base edit form, provides ajaxified saving
* and reloading itself through the ajax return values.
* Takes care of resizing tabsets within the layout container.
*
* Change tracking is enabled on all fields within the form. If you want
* to disable change tracking for a specific field, add a "no-change-track"
* class to it.
*
* @name ss.Form_EditForm
* @require jquery.changetracker
*
* Events:
* ajaxsubmit - Form is about to be submitted through ajax
* validate - Contains validation result
* load - Form is about to be loaded through ajax
*/
$('.cms-edit-form').entwine(/** @lends ss.Form_EditForm */{
/**
* Variable: PlaceholderHtml
* (String_ HTML text to show when no form content is chosen.
* Will show inside the <form> tag.
*/
PlaceholderHtml: '',
/**
* Variable: ChangeTrackerOptions
* (Object)
*/
ChangeTrackerOptions: {
ignoreFieldSelector: '.no-change-track, .ss-upload :input, .cms-navigator :input'
},
/**
* Constructor: onmatch
*/
onadd: function() {
var self = this;
// Turn off autocomplete to fix the access tab randomly switching radio buttons in Firefox
// when refresh the page with an anchor tag in the URL. E.g: /admin#Root_Access.
// Autocomplete in the CMS also causes strangeness in other browsers,
// filling out sections of the form that the user does not want to be filled out,
// so this turns it off for all browsers.
// See the following page for demo and explanation of the Firefox bug:
// http://www.ryancramer.com/journal/entries/radio_buttons_firefox/
this.attr("autocomplete", "off");
this._setupChangeTracker();
// Catch navigation events before they reach handleStateChange(),
// in order to avoid changing the menu state if the action is cancelled by the user
// $('.cms-menu')
// Optionally get the form attributes from embedded fields, see Form->formHtmlContent()
for(var overrideAttr in {'action':true,'method':true,'enctype':true,'name':true}) {
var el = this.find(':input[name='+ '_form_' + overrideAttr + ']');
if(el) {
this.attr(overrideAttr, el.val());
el.remove();
}
}
// TODO
// // Rewrite # links
// html = html.replace(/(<a[^>]+href *= *")#/g, '$1' + window.location.href.replace(/#.*$/,'') + '#');
//
// // Rewrite iframe links (for IE)
// html = html.replace(/(<iframe[^>]*src=")([^"]+)("[^>]*>)/g, '$1' + $('base').attr('href') + '$2$3');
// Show validation errors if necessary
if(this.hasClass('validationerror')) {
// Ensure the first validation error is visible
var tabError = this.find('.message.validation, .message.required').first().closest('.tab');
$('.cms-container').clearCurrentTabState(); // clear state to avoid override later on
tabError.closest('.ss-tabset').tabs('option', 'active', tabError.index('.tab'));
}
this._super();
},
onremove: function() {
this.changetracker('destroy');
this._super();
},
onmatch: function() {
this._super();
},
onunmatch: function() {
this._super();
},
redraw: function() {
if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
// Force initialization of tabsets to avoid layout glitches
this.add(this.find('.cms-tabset')).redrawTabs();
this.find('.cms-content-header').redraw();
},
/**
* Function: _setupChangeTracker
*/
_setupChangeTracker: function() {
// Don't bind any events here, as we dont replace the
// full <form> tag by any ajax updates they won't automatically reapply
this.changetracker(this.getChangeTrackerOptions());
},
/**
* Function: confirmUnsavedChanges
*
* Checks the jquery.changetracker plugin status for this form,
* and asks the user for confirmation via a browser dialog if changes are detected.
* Doesn't cancel any unload or form removal events, you'll need to implement this based on the return
* value of this message.
*
* If changes are confirmed for discard, the 'changed' flag is reset.
*
* Returns:
* (Boolean) FALSE if the user wants to abort with changes present, TRUE if no changes are detected
* or the user wants to discard them.
*/
confirmUnsavedChanges: function() {
this.trigger('beforesubmitform');
if(!this.is('.changed')) {
return true;
}
var confirmed = confirm(ss.i18n._t('LeftAndMain.CONFIRMUNSAVED'));
if(confirmed) {
// confirm discard changes
this.removeClass('changed');
}
return confirmed;
},
/**
* Function: onsubmit
*
* Suppress submission unless it is handled through ajaxSubmit().
*/
onsubmit: function(e, button) {
// Only submit if a button is present.
// This supressed submits from ENTER keys in input fields,
// which means the browser auto-selects the first available form button.
// This might be an unrelated button of the form field,
// or a destructive action (if "save" is not available, or not on first position).
if(this.prop("target") != "_blank") {
if(button) this.closest('.cms-container').submitForm(this, button);
return false;
}
},
/**
* Function: validate
*
* Hook in (optional) validation routines.
* Currently clientside validation is not supported out of the box in the CMS.
*
* Todo:
* Placeholder implementation
*
* Returns:
* {boolean}
*/
validate: function() {
var isValid = true;
this.trigger('validate', {isValid: isValid});
return isValid;
},
/*
* Track focus on htmleditor fields
*/
'from .htmleditor': {
oneditorinit: function(e){
var self = this,
field = $(e.target).closest('.field.htmleditor'),
editor = field.find('textarea.htmleditor').getEditor().getInstance();
// TinyMCE 4 will add a focus event, but for now, use click
editor.onClick.add(function(e){
self.saveFieldFocus(field.attr('id'));
});
}
},
/*
* Track focus on inputs
*/
'from .cms-edit-form :input:not(:submit)': {
onclick: function(e){
this.saveFieldFocus($(e.target).attr('id'));
},
onfocus: function(e){
this.saveFieldFocus($(e.target).attr('id'));
}
},
/*
* Track focus on treedropdownfields.
*/
'from .cms-edit-form .treedropdown *': {
onfocusin: function(e){
var field = $(e.target).closest('.field.treedropdown');
this.saveFieldFocus(field.attr('id'));
}
},
/*
* Track focus on chosen selects
*/
'from .cms-edit-form .dropdown .chzn-container a': {
onfocusin: function(e){
var field = $(e.target).closest('.field.dropdown');
this.saveFieldFocus(field.attr('id'));
}
},
/*
* Restore fields after tabs are restored
*/
'from .cms-container': {
ontabstaterestored: function(e){
this.restoreFieldFocus();
}
},
/*
* Saves focus in Window session storage so it that can be restored on page load
*/
saveFieldFocus: function(selected){
if(typeof(window.sessionStorage)=="undefined" || window.sessionStorage === null) return;
var id = $(this).attr('id'),
focusElements = [];
focusElements.push({
id:id,
selected:selected
});
if(focusElements) {
try {
window.sessionStorage.setItem(id, JSON.stringify(focusElements));
} catch(err) {
if (err.code === DOMException.QUOTA_EXCEEDED_ERR && window.sessionStorage.length === 0) {
// If this fails we ignore the error as the only issue is that it
// does not remember the focus state.
// This is a Safari bug which happens when private browsing is enabled.
return;
} else {
throw err;
}
}
}
},
/**
* Set focus or window to previously saved fields.
* Requires HTML5 sessionStorage support.
*
* Must follow tab restoration, as reliant on active tab
*/
restoreFieldFocus: function(){
if(typeof(window.sessionStorage)=="undefined" || window.sessionStorage === null) return;
var self = this,
hasSessionStorage = (typeof(window.sessionStorage)!=="undefined" && window.sessionStorage),
sessionData = hasSessionStorage ? window.sessionStorage.getItem(this.attr('id')) : null,
sessionStates = sessionData ? JSON.parse(sessionData) : false,
elementID,
tabbed = (this.find('.ss-tabset').length !== 0),
activeTab,
elementTab,
toggleComposite,
scrollY;
if(hasSessionStorage && sessionStates.length > 0){
$.each(sessionStates, function(i, sessionState) {
if(self.is('#' + sessionState.id)){
elementID = $('#' + sessionState.selected);
}
});
// If the element IDs saved in session states don't match up to anything in this particular form
// that probably means we haven't encountered this form yet, so focus on the first input
if($(elementID).length < 1){
this.focusFirstInput();
return;
}
activeTab = $(elementID).closest('.ss-tabset').find('.ui-tabs-nav .ui-tabs-active .ui-tabs-anchor').attr('id');
elementTab = 'tab-' + $(elementID).closest('.ss-tabset .ui-tabs-panel').attr('id');
// Last focussed element differs to last selected tab, do nothing
if(tabbed && elementTab !== activeTab){
return;
}
toggleComposite = $(elementID).closest('.togglecomposite');
//Reopen toggle fields
if(toggleComposite.length > 0){
toggleComposite.accordion('activate', toggleComposite.find('.ui-accordion-header'));
}
//Calculate position for scroll
scrollY = $(elementID).position().top;
//Fall back to nearest visible element if hidden (for select type fields)
if(!$(elementID).is(':visible')){
elementID = '#' + $(elementID).closest('.field').attr('id');
scrollY = $(elementID).position().top;
}
//set focus to focus variable if element focusable
$(elementID).focus();
// Scroll fallback when element is not focusable
// Only scroll if element at least half way down window
if(scrollY > $(window).height() / 2){
self.find('.cms-content-fields').scrollTop(scrollY);
}
} else {
// If session storage is not supported or there is nothing stored yet, focus on the first input
this.focusFirstInput();
}
},
/**
* Skip if an element in the form is already focused. Exclude elements which specifically
* opt-out of this behaviour via "data-skip-autofocus". This opt-out is useful if the
* first visible field is shown far down a scrollable area, for example for the pagination
* input field after a long GridField listing.
*/
focusFirstInput: function() {
this.find(':input:not(:submit)[data-skip-autofocus!="true"]').filter(':visible:first').focus();
}
});
/**
* Class: .cms-edit-form .Actions :submit
*
* All buttons in the right CMS form go through here by default.
* We need this onclick overloading because we can't get to the
* clicked button from a form.onsubmit event.
*/
$('.cms-edit-form .Actions input.action[type=submit], .cms-edit-form .Actions button.action').entwine({
/**
* Function: onclick
*/
onclick: function(e) {
// Confirmation on delete.
if(
this.hasClass('gridfield-button-delete')
&& !confirm(ss.i18n._t('TABLEFIELD.DELETECONFIRMMESSAGE'))
) {
e.preventDefault();
return false;
}
if(!this.is(':disabled')) {
this.parents('form').trigger('submit', [this]);
}
e.preventDefault();
return false;
}
});
/**
* If we've a history state to go back to, go back, otherwise fall back to
* submitting the form with the 'doCancel' action.
*/
$('.cms-edit-form .Actions input.action[type=submit].ss-ui-action-cancel, .cms-edit-form .Actions button.action.ss-ui-action-cancel').entwine({
onclick: function(e) {
if (History.getStateByIndex(1)) {
History.back();
} else {
this.parents('form').trigger('submit', [this]);
}
e.preventDefault();
}
});
/**
* Hide tabs when only one is available.
* Special case is actiontabs - tabs between buttons, where we want to have
* extra options hidden within a tab (even if only one) by default.
*/
$('.cms-edit-form .ss-tabset').entwine({
onmatch: function() {
if (!this.hasClass('ss-ui-action-tabset')) {
var tabs = this.find("> ul:first");
if(tabs.children("li").length == 1) {
tabs.hide().parent().addClass("ss-tabset-tabshidden");
}
}
this._super();
},
onunmatch: function() {
this._super();
}
});
});
}(jQuery));

View File

@ -1,180 +0,0 @@
/**
* File: LeftAndMain.Layout.js
*/
(function($) {
$.fn.layout.defaults.resize = false;
/**
* Acccess the global variable in the same way the plugin does it.
*/
jLayout = (typeof jLayout === 'undefined') ? {} : jLayout;
/**
* Factory function for generating new type of algorithm for our CMS.
*
* Spec requires a definition of three column elements:
* - `menu` on the left
* - `content` area in the middle (includes the EditForm, side tool panel, actions, breadcrumbs and tabs)
* - `preview` on the right (will be shown if there is enough space)
*
* Required options:
* - `minContentWidth`: minimum size for the content display as long as the preview is visible
* - `minPreviewWidth`: preview will not be displayed below this size
* - `mode`: one of "split", "content" or "preview"
*
* The algorithm first checks which columns are to be visible and which hidden.
*
* In the case where both preview and content should be shown it first tries to assign half of non-menu space to
* preview and the other half to content. Then if there is not enough space for either content or preview, it tries
* to allocate the minimum acceptable space to that column, and the rest to the other one. If the minimum
* requirements are still not met, it falls back to showing content only.
*
* @param spec A structure defining columns and parameters as per above.
*/
jLayout.threeColumnCompressor = function (spec, options) {
// Spec sanity checks.
if (typeof spec.menu==='undefined' ||
typeof spec.content==='undefined' ||
typeof spec.preview==='undefined') {
throw 'Spec is invalid. Please provide "menu", "content" and "preview" elements.';
}
if (typeof options.minContentWidth==='undefined' ||
typeof options.minPreviewWidth==='undefined' ||
typeof options.mode==='undefined') {
throw 'Spec is invalid. Please provide "minContentWidth", "minPreviewWidth", "mode"';
}
if (options.mode!=='split' && options.mode!=='content' && options.mode!=='preview') {
throw 'Spec is invalid. "mode" should be either "split", "content" or "preview"';
}
// Instance of the algorithm being produced.
var obj = {
options: options
};
// Internal column handles, also implementing layout.
var menu = $.jLayoutWrap(spec.menu),
content = $.jLayoutWrap(spec.content),
preview = $.jLayoutWrap(spec.preview);
/**
* Required interface implementations follow.
* Refer to https://github.com/bramstein/jlayout#layout-algorithms for the interface spec.
*/
obj.layout = function (container) {
var size = container.bounds(),
insets = container.insets(),
top = insets.top,
bottom = size.height - insets.bottom,
left = insets.left,
right = size.width - insets.right;
var menuWidth = spec.menu.width(),
contentWidth = 0,
previewWidth = 0;
if (this.options.mode==='preview') {
// All non-menu space allocated to preview.
contentWidth = 0;
previewWidth = right - left - menuWidth;
} else if (this.options.mode==='content') {
// All non-menu space allocated to content.
contentWidth = right - left - menuWidth;
previewWidth = 0;
} else { // ==='split'
// Split view - first try 50-50 distribution.
contentWidth = (right - left - menuWidth) / 2;
previewWidth = right - left - (menuWidth + contentWidth);
// If violating one of the minima, try to readjust towards satisfying it.
if (contentWidth < this.options.minContentWidth) {
contentWidth = this.options.minContentWidth;
previewWidth = right - left - (menuWidth + contentWidth);
} else if (previewWidth < this.options.minPreviewWidth) {
previewWidth = this.options.minPreviewWidth;
contentWidth = right - left - (menuWidth + previewWidth);
}
// If still violating one of the (other) minima, remove the preview and allocate everything to content.
if (contentWidth < this.options.minContentWidth || previewWidth < this.options.minPreviewWidth) {
contentWidth = right - left - menuWidth;
previewWidth = 0;
}
}
// Calculate what columns are already hidden pre-layout
var prehidden = {
content: spec.content.hasClass('column-hidden'),
preview: spec.preview.hasClass('column-hidden')
};
// Calculate what columns will be hidden (zero width) post-layout
var posthidden = {
content: contentWidth === 0,
preview: previewWidth === 0
};
// Apply classes for elements that might not be visible at all.
spec.content.toggleClass('column-hidden', posthidden.content);
spec.preview.toggleClass('column-hidden', posthidden.preview);
// Apply the widths to columns, and call subordinate layouts to arrange the children.
menu.bounds({'x': left, 'y': top, 'height': bottom - top, 'width': menuWidth});
menu.doLayout();
left += menuWidth;
content.bounds({'x': left, 'y': top, 'height': bottom - top, 'width': contentWidth});
if (!posthidden.content) content.doLayout();
left += contentWidth;
preview.bounds({'x': left, 'y': top, 'height': bottom - top, 'width': previewWidth});
if (!posthidden.preview) preview.doLayout();
if (posthidden.content !== prehidden.content) spec.content.trigger('columnvisibilitychanged');
if (posthidden.preview !== prehidden.preview) spec.preview.trigger('columnvisibilitychanged');
// Calculate whether preview is possible in split mode
if (contentWidth + previewWidth < options.minContentWidth + options.minPreviewWidth) {
spec.preview.trigger('disable');
} else {
spec.preview.trigger('enable');
}
return container;
};
/**
* Helper to generate the required `preferred`, `minimum` and `maximum` interface functions.
*/
function typeLayout(type) {
var func = type + 'Size';
return function (container) {
var menuSize = menu[func](),
contentSize = content[func](),
previewSize = preview[func](),
insets = container.insets();
width = menuSize.width + contentSize.width + previewSize.width;
height = Math.max(menuSize.height, contentSize.height, previewSize.height);
return {
'width': insets.left + insets.right + width,
'height': insets.top + insets.bottom + height
};
};
}
// Generate interface functions.
obj.preferred = typeLayout('preferred');
obj.minimum = typeLayout('minimum');
obj.maximum = typeLayout('maximum');
return obj;
};
}(jQuery));

View File

@ -1,419 +0,0 @@
(function($) {
$.entwine('ss', function($){
/**
* Vertical CMS menu with two levels, built from a nested unordered list.
* The (optional) second level is collapsible, hiding its children.
* The whole menu (including second levels) is collapsible as well,
* exposing only a preview for every menu item in order to save space.
* In this "preview/collapsed" mode, the secondary menu hovers over the menu item,
* rather than expanding it.
*
* Example:
*
* <ul class="cms-menu-list">
* <li><a href="#">Item 1</a></li>
* <li class="current opened">
* <a href="#">Item 2</a>
* <ul>
* <li class="current opened"><a href="#">Item 2.1</a></li>
* <li><a href="#">Item 2.2</a></li>
* </ul>
* </li>
* </ul>
*
* Custom Events:
* - 'select': Fires when a menu item is selected (on any level).
*/
$('.cms-panel.cms-menu').entwine({
togglePanel: function(doExpand, silent, doSaveState) {
//apply or unapply the flyout formatting, should only apply to cms-menu-list when the current collapsed panal is the cms menu.
$('.cms-menu-list').children('li').each(function(){
if (doExpand) { //expand
$(this).children('ul').each(function() {
$(this).removeClass('collapsed-flyout');
if ($(this).data('collapse')) {
$(this).removeData('collapse');
$(this).addClass('collapse');
}
});
} else { //collapse
$(this).children('ul').each(function() {
$(this).addClass('collapsed-flyout');
$(this).hasClass('collapse');
$(this).removeClass('collapse');
$(this).data('collapse', true);
});
}
});
this.toggleFlyoutState(doExpand);
this._super(doExpand, silent, doSaveState);
},
toggleFlyoutState: function(bool) {
if (bool) { //expand
//show the flyout
$('.collapsed').find('li').show();
//hide all the flyout-indicator
$('.cms-menu-list').find('.child-flyout-indicator').hide();
} else { //collapse
//hide the flyout only if it is not the current section
$('.collapsed-flyout').find('li').each(function() {
//if (!$(this).hasClass('current'))
$(this).hide();
});
//show all the flyout-indicators
var par = $('.cms-menu-list ul.collapsed-flyout').parent();
if (par.children('.child-flyout-indicator').length === 0) par.append('<span class="child-flyout-indicator"></span>').fadeIn();
par.children('.child-flyout-indicator').fadeIn();
}
},
siteTreePresent: function () {
return $('#cms-content-tools-CMSMain').length > 0;
},
/**
* @func getPersistedStickyState
* @return {boolean|undefined} - Returns true if the menu is sticky, false if unsticky. Returns undefined if there is no cookie set.
* @desc Get the sticky state of the menu according to the cookie.
*/
getPersistedStickyState: function () {
var persistedState, cookieValue;
if ($.cookie !== void 0) {
cookieValue = $.cookie('cms-menu-sticky');
if (cookieValue !== void 0 && cookieValue !== null) {
persistedState = cookieValue === 'true';
}
}
return persistedState;
},
/**
* @func setPersistedStickyState
* @param {boolean} isSticky - Pass true if you want the panel to be sticky, false for unsticky.
* @desc Set the collapsed value of the panel, stored in cookies.
*/
setPersistedStickyState: function (isSticky) {
if ($.cookie !== void 0) {
$.cookie('cms-menu-sticky', isSticky, { path: '/', expires: 31 });
}
},
/**
* @func getEvaluatedCollapsedState
* @return {boolean} - Returns true if the menu should be collapsed, false if expanded.
* @desc Evaluate whether the menu should be collapsed.
* The basic rule is "If the SiteTree (middle column) is present, collapse the menu, otherwise expand the menu".
* This reason behind this is to give the content area more real estate when the SiteTree is present.
* The user may wish to override this automatic behaviour and have the menu expanded or collapsed at all times.
* So unlike manually toggling the menu, the automatic behaviour never updates the menu's cookie value.
* Here we use the manually set state and the automatic behaviour to evaluate what the collapsed state should be.
*/
getEvaluatedCollapsedState: function () {
var shouldCollapse,
manualState = this.getPersistedCollapsedState(),
menuIsSticky = $('.cms-menu').getPersistedStickyState(),
automaticState = this.siteTreePresent();
if (manualState === void 0) {
// There is no manual state, use automatic state.
shouldCollapse = automaticState;
} else if (manualState !== automaticState && menuIsSticky) {
// The manual and automatic statea conflict, use manual state.
shouldCollapse = manualState;
} else {
// Use automatic state.
shouldCollapse = automaticState;
}
return shouldCollapse;
},
onadd: function () {
var self = this;
setTimeout(function () {
// Use a timeout so this happens after the redraw.
// Triggering a toggle before redraw will result in an incorrect
// menu 'expanded width' being calculated when then menu
// is added in a collapsed state.
self.togglePanel(!self.getEvaluatedCollapsedState(), false, false);
}, 0);
// Setup automatic expand / collapse behaviour.
$(window).on('ajaxComplete', function (e) {
setTimeout(function () { // Use a timeout so this happens after the redraw
self.togglePanel(!self.getEvaluatedCollapsedState(), false, false);
}, 0);
});
this._super();
}
});
$('.cms-menu-list').entwine({
onmatch: function() {
var self = this;
// Select default element (which might reveal children in hidden parents)
this.find('li.current').select();
this.updateItems();
this._super();
},
onunmatch: function() {
this._super();
},
updateMenuFromResponse: function(xhr) {
var controller = xhr.getResponseHeader('X-Controller');
if(controller) {
var item = this.find('li#Menu-' + controller.replace(/\\/g, '-').replace(/[^a-zA-Z0-9\-_:.]+/, ''));
if(!item.hasClass('current')) item.select();
}
this.updateItems();
},
'from .cms-container': {
onafterstatechange: function(e, data){
this.updateMenuFromResponse(data.xhr);
},
onaftersubmitform: function(e, data){
this.updateMenuFromResponse(data.xhr);
}
},
'from .cms-edit-form': {
onrelodeditform: function(e, data){
this.updateMenuFromResponse(data.xmlhttp);
}
},
getContainingPanel: function(){
return this.closest('.cms-panel');
},
fromContainingPanel: {
ontoggle: function(e){
this.toggleClass('collapsed', $(e.target).hasClass('collapsed'));
}
},
updateItems: function() {
// Hide "edit page" commands unless the section is activated
var editPageItem = this.find('#Menu-CMSMain');
editPageItem[editPageItem.is('.current') ? 'show' : 'hide']();
// Update the menu links to reflect the page ID if the page has changed the URL.
var currentID = $('.cms-content input[name=ID]').val();
if(currentID) {
this.find('li').each(function() {
if($.isFunction($(this).setRecordID)) $(this).setRecordID(currentID);
});
}
}
});
/** Toggle the flyout panel to appear/disappear when mouse over */
$('.cms-menu-list li').entwine({
toggleFlyout: function(bool) {
fly = $(this);
if (fly.children('ul').first().hasClass('collapsed-flyout')) {
if (bool) { //expand
fly.children('ul').find('li').fadeIn('fast');
} else { //collapse
fly.children('ul').find('li').hide();
}
}
}
});
//slight delay to prevent flyout closing from "sloppy mouse movement"
$('.cms-menu-list li').hoverIntent(function(){$(this).toggleFlyout(true);},function(){$(this).toggleFlyout(false);});
$('.cms-menu-list .toggle').entwine({
onclick: function(e) {
this.getMenuItem().toggle();
e.preventDefault();
}
});
$('.cms-menu-list li').entwine({
onmatch: function() {
if(this.find('ul').length) {
this.find('a:first').append('<span class="toggle-children"><span class="toggle-children-icon"></span></span>');
}
this._super();
},
onunmatch: function() {
this._super();
},
toggle: function() {
this[this.hasClass('opened') ? 'close' : 'open']();
},
/**
* "Open" is just a visual state, and unrelated to "current".
* More than one item can be open at the same time.
*/
open: function() {
var parent = this.getMenuItem();
if(parent) parent.open();
this.addClass('opened').find('ul').show();
this.find('.toggle-children').addClass('opened');
},
close: function() {
this.removeClass('opened').find('ul').hide();
this.find('.toggle-children').removeClass('opened');
},
select: function() {
var parent = this.getMenuItem();
this.addClass('current').open();
// Remove "current" class from all siblings and their children
this.siblings().removeClass('current').close();
this.siblings().find('li').removeClass('current');
if(parent) {
var parentSiblings = parent.siblings();
parent.addClass('current');
parentSiblings.removeClass('current').close();
parentSiblings.find('li').removeClass('current').close();
}
this.getMenu().updateItems();
this.trigger('select');
}
});
$('.cms-menu-list *').entwine({
getMenu: function() {
return this.parents('.cms-menu-list:first');
}
});
$('.cms-menu-list li *').entwine({
getMenuItem: function() {
return this.parents('li:first');
}
});
/**
* Both primary and secondary nav.
*/
$('.cms-menu-list li a').entwine({
onclick: function(e) {
// Only catch left clicks, in order to allow opening in tabs.
// Ignore external links, fallback to standard link behaviour
var isExternal = $.path.isExternal(this.attr('href'));
if(e.which > 1 || isExternal) return;
// if the developer has this to open in a new window, handle
// that
if(this.attr('target') == "_blank") {
return;
}
e.preventDefault();
var item = this.getMenuItem();
var url = this.attr('href');
if(!isExternal) url = $('base').attr('href') + url;
var children = item.find('li');
if(children.length) {
children.first().find('a').click();
} else {
// Load URL, but give the loading logic an opportunity to veto the action
// (e.g. because of unsaved changes)
if(!$('.cms-container').loadPanel(url)) return false;
}
item.select();
}
});
$('.cms-menu-list li .toggle-children').entwine({
onclick: function(e) {
var li = this.closest('li');
li.toggle();
return false; // prevent wrapping link event to fire
}
});
$('.cms .profile-link').entwine({
onclick: function() {
$('.cms-container').loadPanel(this.attr('href'));
$('.cms-menu-list li').removeClass('current').close();
return false;
}
});
/**
* Toggles the manual override of the left menu's automatic expand / collapse behaviour.
*/
$('.cms-menu .sticky-toggle').entwine({
onadd: function () {
var isSticky = $('.cms-menu').getPersistedStickyState() ? true : false;
this.toggleCSS(isSticky);
this.toggleIndicator(isSticky);
this._super();
},
/**
* @func toggleCSS
* @param {boolean} isSticky - The current state of the menu.
* @desc Toggles the 'active' CSS class of the element.
*/
toggleCSS: function (isSticky) {
this[isSticky ? 'addClass' : 'removeClass']('active');
},
/**
* @func toggleIndicator
* @param {boolean} isSticky - The current state of the menu.
* @desc Updates the indicator's text based on the sticky state of the menu.
*/
toggleIndicator: function (isSticky) {
this.next('.sticky-status-indicator').text(isSticky ? 'fixed' : 'auto');
},
onclick: function () {
var $menu = this.closest('.cms-menu'),
persistedCollapsedState = $menu.getPersistedCollapsedState(),
persistedStickyState = $menu.getPersistedStickyState(),
newStickyState = persistedStickyState === void 0 ? !this.hasClass('active') : !persistedStickyState;
// Update the persisted collapsed state
if (persistedCollapsedState === void 0) {
// If there is no persisted menu state currently set, then set it to the menu's current state.
// This will be the case if the user has never manually expanded or collapsed the menu,
// or the menu has previously been made unsticky.
$menu.setPersistedCollapsedState($menu.hasClass('collapsed'));
} else if (persistedCollapsedState !== void 0 && newStickyState === false) {
// If there is a persisted state and the menu has been made unsticky, remove the persisted state.
$menu.clearPersistedCollapsedState();
}
// Persist the sticky state of the menu
$menu.setPersistedStickyState(newStickyState);
this.toggleCSS(newStickyState);
this.toggleIndicator(newStickyState);
this._super();
}
});
});
}(jQuery));

View File

@ -1,220 +0,0 @@
(function($) {
$.entwine('ss', function($) {
// setup jquery.entwine
$.entwine.warningLevel = $.entwine.WARN_LEVEL_BESTPRACTISE;
/**
* Horizontal collapsible panel. Generic enough to work with CMS menu as well as various "filter" panels.
*
* A panel consists of the following parts:
* - Container div: The outer element, with class ".cms-panel"
* - Header (optional)
* - Content
* - Expand and collapse toggle anchors (optional)
*
* Sample HTML:
* <div class="cms-panel">
* <div class="cms-panel-header">your header</div>
* <div class="cms-panel-content">your content here</div>
* <div class="cms-panel-toggle">
* <a href="#" class="toggle-expande">your toggle text</a>
* <a href="#" class="toggle-collapse">your toggle text</a>
* </div>
* </div>
*/
$('.cms-panel').entwine({
WidthExpanded: null,
WidthCollapsed: null,
/**
* @func canSetCookie
* @return {boolean}
* @desc Before trying to set a cookie, make sure $.cookie and the element's id are both defined.
*/
canSetCookie: function () {
return $.cookie !== void 0 && this.attr('id') !== void 0;
},
/**
* @func getPersistedCollapsedState
* @return {boolean|undefined} - Returns true if the panel is collapsed, false if expanded. Returns undefined if there is no cookie set.
* @desc Get the collapsed state of the panel according to the cookie.
*/
getPersistedCollapsedState: function () {
var isCollapsed, cookieValue;
if (this.canSetCookie()) {
cookieValue = $.cookie('cms-panel-collapsed-' + this.attr('id'));
if (cookieValue !== void 0 && cookieValue !== null) {
isCollapsed = cookieValue === 'true';
}
}
return isCollapsed;
},
/**
* @func setPersistedCollapsedState
* @param {boolean} newState - Pass true if you want the panel to be collapsed, false for expanded.
* @desc Set the collapsed value of the panel, stored in cookies.
*/
setPersistedCollapsedState: function (newState) {
if (this.canSetCookie()) {
$.cookie('cms-panel-collapsed-' + this.attr('id'), newState, { path: '/', expires: 31 });
}
},
/**
* @func clearPersistedState
* @desc Remove the cookie responsible for maintaing the collapsed state.
*/
clearPersistedCollapsedState: function () {
if (this.canSetCookie()) {
$.cookie('cms-panel-collapsed-' + this.attr('id'), '', { path: '/', expires: -1 });
}
},
/**
* @func getInitialCollapsedState
* @return {boolean} - Returns true if the the panel is collapsed, false if expanded.
* @desc Get the initial collapsed state of the panel. Check if a cookie value is set then fall back to checking CSS classes.
*/
getInitialCollapsedState: function () {
var isCollapsed = this.getPersistedCollapsedState();
// Fallback to getting the state from the default CSS class
if (isCollapsed === void 0) {
isCollapsed = this.hasClass('collapsed');
}
return isCollapsed;
},
onadd: function() {
var collapsedContent, container;
if(!this.find('.cms-panel-content').length) throw new Exception('Content panel for ".cms-panel" not found');
// Create default controls unless they already exist.
if(!this.find('.cms-panel-toggle').length) {
container = $("<div class='cms-panel-toggle south'></div>")
.append('<a class="toggle-expand" href="#"><span>&raquo;</span></a>')
.append('<a class="toggle-collapse" href="#"><span>&laquo;</span></a>');
this.append(container);
}
// Set panel width same as the content panel it contains. Assumes the panel has overflow: hidden.
this.setWidthExpanded(this.find('.cms-panel-content').innerWidth());
// Assumes the collapsed width is indicated by the toggle, or by an optionally collapsed view
collapsedContent = this.find('.cms-panel-content-collapsed');
this.setWidthCollapsed(collapsedContent.length ? collapsedContent.innerWidth() : this.find('.toggle-expand').innerWidth());
// Toggle visibility
this.togglePanel(!this.getInitialCollapsedState(), true, false);
this._super();
},
/**
* @func togglePanel
* @param doExpand {boolean} - true to expand, false to collapse.
* @param silent {boolean} - true means that events won't be fired, which is useful for the component initialization phase.
* @param doSaveState - if false, the panel's state will not be persisted via cookies.
* @desc Toggle the expanded / collapsed state of the panel.
*/
togglePanel: function(doExpand, silent, doSaveState) {
var newWidth, collapsedContent;
if(!silent) {
this.trigger('beforetoggle.sspanel', doExpand);
this.trigger(doExpand ? 'beforeexpand' : 'beforecollapse');
}
this.toggleClass('collapsed', !doExpand);
newWidth = doExpand ? this.getWidthExpanded() : this.getWidthCollapsed();
this.width(newWidth); // the content panel width always stays in "expanded state" to avoid floating elements
// If an alternative collapsed view exists, toggle it as well
collapsedContent = this.find('.cms-panel-content-collapsed');
if(collapsedContent.length) {
this.find('.cms-panel-content')[doExpand ? 'show' : 'hide']();
this.find('.cms-panel-content-collapsed')[doExpand ? 'hide' : 'show']();
}
if (doSaveState !== false) {
this.setPersistedCollapsedState(!doExpand);
}
// TODO Fix redraw order (inner to outer), and re-enable silent flag
// to avoid multiple expensive redraws on a single load.
// if(!silent) {
this.trigger('toggle', doExpand);
this.trigger(doExpand ? 'expand' : 'collapse');
// }
},
expandPanel: function(force) {
if(!force && !this.hasClass('collapsed')) return;
this.togglePanel(true);
},
collapsePanel: function(force) {
if(!force && this.hasClass('collapsed')) return;
this.togglePanel(false);
}
});
$('.cms-panel.collapsed .cms-panel-toggle').entwine({
onclick: function(e) {
this.expandPanel();
e.preventDefault();
}
});
$('.cms-panel *').entwine({
getPanel: function() {
return this.parents('.cms-panel:first');
}
});
$('.cms-panel .toggle-expand').entwine({
onclick: function(e) {
e.preventDefault();
e.stopPropagation();
this.getPanel().expandPanel();
this._super(e);
}
});
$('.cms-panel .toggle-collapse').entwine({
onclick: function(e) {
e.preventDefault();
e.stopPropagation();
this.getPanel().collapsePanel();
this._super(e);
}
});
$('.cms-content-tools.collapsed').entwine({
// Expand CMS' centre pane, when the pane itself is clicked somewhere
onclick: function(e) {
this.expandPanel();
this._super(e);
}
});
});
}(jQuery));

View File

@ -1,50 +0,0 @@
/**
* File: LeftAndMain.Ping.js
*/
(function($) {
$.entwine('ss.ping', function($){
$('.cms-container').entwine(/** @lends ss.Form_EditForm */{
/**
* Variable: PingIntervalSeconds
* (Number) Interval in which /Security/ping will be checked for a valid login session.
*/
PingIntervalSeconds: 5*60,
onadd: function() {
this._setupPinging();
this._super();
},
/**
* Function: _setupPinging
*
* This function is called by prototype when it receives notification that the user was logged out.
* It uses /Security/ping for this purpose, which should return '1' if a valid user session exists.
* It redirects back to the login form if the URL is either unreachable, or returns '0'.
*/
_setupPinging: function() {
var onSessionLost = function(xmlhttp, status) {
if(xmlhttp.status > 400 || xmlhttp.responseText == 0) {
// TODO will pile up additional alerts when left unattended
if(window.open('Security/login')) {
alert('Please log in and then try again');
} else {
alert('Please enable pop-ups for this site');
}
}
};
// setup pinging for login expiry
setInterval(function() {
$.ajax({
url: 'Security/ping',
global: false,
type: 'POST',
complete: onSessionLost
});
}, this.getPingIntervalSeconds() * 1000);
}
});
});
}(jQuery));

View File

@ -1,848 +0,0 @@
(function($) {
$.entwine('ss.preview', function($){
/**
* Shows a previewable website state alongside its editable version in backend UI.
*
* Relies on the server responses to indicate if a preview is available for the
* currently loaded admin interface - signified by class ".cms-previewable" being present.
*
* The preview options at the bottom are constructured by grabbing a SilverStripeNavigator
* structure also provided by the backend.
*/
$('.cms-preview').entwine({
/**
* List of SilverStripeNavigator states (SilverStripeNavigatorItem classes) to search for.
* The order is significant - if the state is not available, preview will start searching the list
* from the beginning.
*/
AllowedStates: ['StageLink', 'LiveLink','ArchiveLink'],
/**
* API
* Name of the current preview state - one of the "AllowedStates".
*/
CurrentStateName: null,
/**
* API
* Current size selection.
*/
CurrentSizeName: 'auto',
/**
* Flags whether the preview is available on this CMS section.
*/
IsPreviewEnabled: false,
/**
* Mode in which the preview will be enabled.
*/
DefaultMode: 'split',
Sizes: {
auto: {
width: '100%',
height: '100%'
},
mobile: {
width: '335px', // add 15px for approx desktop scrollbar
height: '568px'
},
mobileLandscape: {
width: '583px', // add 15px for approx desktop scrollbar
height: '320px'
},
tablet: {
width: '783px', // add 15px for approx desktop scrollbar
height: '1024px'
},
tabletLandscape: {
width: '1039px', // add 15px for approx desktop scrollbar
height: '768px'
},
desktop: {
width: '1024px',
height: '800px'
}
},
/**
* API
* Switch the preview to different state.
* stateName can be one of the "AllowedStates".
*
* @param {String}
* @param {Boolean} Set to FALSE to avoid persisting the state
*/
changeState: function(stateName, save) {
var self = this, states = this._getNavigatorStates();
if(save !== false) {
$.each(states, function(index, state) {
self.saveState('state', stateName);
});
}
this.setCurrentStateName(stateName);
this._loadCurrentState();
this.redraw();
return this;
},
/**
* API
* Change the preview mode.
* modeName can be: split, content, preview.
*/
changeMode: function(modeName, save) {
var container = $('.cms-container');
if (modeName == 'split') {
container.entwine('.ss').splitViewMode();
this.setIsPreviewEnabled(true);
this._loadCurrentState();
} else if (modeName == 'content') {
container.entwine('.ss').contentViewMode();
this.setIsPreviewEnabled(false);
// Do not load content as the preview is not visible.
} else if (modeName == 'preview') {
container.entwine('.ss').previewMode();
this.setIsPreviewEnabled(true);
this._loadCurrentState();
} else {
throw 'Invalid mode: ' + modeName;
}
if(save !== false) this.saveState('mode', modeName);
this.redraw();
return this;
},
/**
* API
* Change the preview size.
* sizeName can be: auto, desktop, tablet, mobile.
*/
changeSize: function(sizeName) {
var sizes = this.getSizes();
this.setCurrentSizeName(sizeName);
this.removeClass('auto desktop tablet mobile').addClass(sizeName);
this.find('.preview-device-outer')
.width(sizes[sizeName].width)
.height(sizes[sizeName].height);
this.find('.preview-device-inner')
.width(sizes[sizeName].width);
this.saveState('size', sizeName);
this.redraw();
return this;
},
/**
* API
* Update the visual appearance to match the internal preview state.
*/
redraw: function() {
if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
// Update preview state selector.
var currentStateName = this.getCurrentStateName();
if (currentStateName) {
this.find('.cms-preview-states').changeVisibleState(currentStateName);
}
// Update preview mode selectors.
var layoutOptions = $('.cms-container').entwine('.ss').getLayoutOptions();
if (layoutOptions) {
// There are two mode selectors that we need to keep in sync. Redraw both.
$('.preview-mode-selector').changeVisibleMode(layoutOptions.mode);
}
// Update preview size selector.
var currentSizeName = this.getCurrentSizeName();
if (currentSizeName) {
this.find('.preview-size-selector').changeVisibleSize(this.getCurrentSizeName());
}
return this;
},
/**
* Store the preview options for this page.
*/
saveState : function(name, value) {
if(this._supportsLocalStorage()) window.localStorage.setItem('cms-preview-state-' + name, value);
},
/**
* Load previously stored preferences
*/
loadState : function(name) {
if(this._supportsLocalStorage()) return window.localStorage.getItem('cms-preview-state-' + name);
},
/**
* Disable the area - it will not appear in the GUI.
* Caveat: the preview will be automatically enabled when ".cms-previewable" class is detected.
*/
disablePreview: function() {
this.setPendingURL(null);
this._loadUrl('about:blank');
this._block();
this.changeMode('content', false);
this.setIsPreviewEnabled(false);
return this;
},
/**
* Enable the area and start updating to reflect the content editing.
*/
enablePreview: function() {
if (!this.getIsPreviewEnabled()) {
this.setIsPreviewEnabled(true);
// Initialise mode.
if ($.browser.msie && $.browser.version.slice(0,3)<=7) {
// We do not support the split mode in IE < 8.
this.changeMode('content');
} else {
this.changeMode(this.getDefaultMode(), false);
}
}
return this;
},
/**
* Return a style element we can use in IE8 to fix fonts (see readystatechange binding in onadd below)
*/
getOrAppendFontFixStyleElement: function() {
var style = $('#FontFixStyleElement');
if (!style.length) {
style = $(
'<style type="text/css" id="FontFixStyleElement" disabled="disabled">'+
':before,:after{content:none !important}'+
'</style>'
).appendTo('head');
}
return style;
},
/**
* Initialise the preview element.
*/
onadd: function() {
var self = this, layoutContainer = this.parent(), iframe = this.find('iframe');
// Create layout and controls
iframe.addClass('center');
iframe.bind('load', function() {
self._adjustIframeForPreview();
// Load edit view for new page, but only if the preview is activated at the moment.
// This avoids e.g. force-redirections of the edit view on RedirectorPage instances.
self._loadCurrentPage();
$(this).removeClass('loading');
});
// If there's any webfonts in the preview, IE8 will start glitching. This fixes that.
if ($.browser.msie && 8 === parseInt($.browser.version, 10)) {
iframe.bind('readystatechange', function(e) {
if(iframe[0].readyState == 'interactive') {
self.getOrAppendFontFixStyleElement().removeAttr('disabled');
setTimeout(function(){ self.getOrAppendFontFixStyleElement().attr('disabled', 'disabled'); }, 0);
}
});
}
// Preview might not be available in all admin interfaces - block/disable when necessary
this.append('<div class="cms-preview-overlay ui-widget-overlay-light"></div>');
this.find('.cms-preview-overlay').hide();
this.disablePreview();
this._super();
},
/**
* Detect and use localStorage if available. In IE11 windows 8.1 call to window.localStorage was throwing out an access denied error in some cases which was causing the preview window not to display correctly in the CMS admin area.
*/
_supportsLocalStorage: function() {
var uid = new Date;
var storage;
var result;
try {
(storage = window.localStorage).setItem(uid, uid);
result = storage.getItem(uid) == uid;
storage.removeItem(uid);
return result && storage;
} catch (exception) {
console.warn('localStorge is not available due to current browser / system settings.');
}
},
onenable: function () {
var $viewModeSelector = $('.preview-mode-selector');
$viewModeSelector.removeClass('split-disabled');
$viewModeSelector.find('.disabled-tooltip').hide();
},
ondisable: function () {
var $viewModeSelector = $('.preview-mode-selector');
$viewModeSelector.addClass('split-disabled');
$viewModeSelector.find('.disabled-tooltip').show();
},
/**
* Set the preview to unavailable - could be still visible. This is purely visual.
*/
_block: function() {
this.addClass('blocked');
this.find('.cms-preview-overlay').show();
return this;
},
/**
* Set the preview to available (remove the overlay);
*/
_unblock: function() {
this.removeClass('blocked');
this.find('.cms-preview-overlay').hide();
return this;
},
/**
* Update the preview according to browser and CMS section capabilities.
*/
_initialiseFromContent: function() {
var mode, size;
if (!$('.cms-previewable').length) {
this.disablePreview();
} else {
mode = this.loadState('mode');
size = this.loadState('size');
this._moveNavigator();
if(!mode || mode != 'content') {
this.enablePreview();
this._loadCurrentState();
}
this.redraw();
// now check the cookie to see if we have any preview settings that have been
// retained for this page from the last visit
if(mode) this.changeMode(mode);
if(size) this.changeSize(size);
}
return this;
},
/**
* Update preview whenever any panels are reloaded.
*/
'from .cms-container': {
onafterstatechange: function(e, data) {
// Don't update preview if we're dealing with a custom redirect
if(data.xhr.getResponseHeader('X-ControllerURL')) return;
this._initialiseFromContent();
}
},
/** @var string A URL that should be displayed in this preview panel once it becomes visible */
PendingURL: null,
oncolumnvisibilitychanged: function() {
var url = this.getPendingURL();
if (url && !this.is('.column-hidden')) {
this.setPendingURL(null);
this._loadUrl(url);
this._unblock();
}
},
/**
* Update preview whenever a form is submitted.
* This is an alternative to the LeftAndmMain::loadPanel functionality which we already
* cover in the onafterstatechange handler.
*/
'from .cms-container .cms-edit-form': {
onaftersubmitform: function(){
this._initialiseFromContent();
}
},
/**
* Change the URL of the preview iframe (if its not already displayed).
*/
_loadUrl: function(url) {
this.find('iframe').addClass('loading').attr('src', url);
return this;
},
/**
* Fetch available states from the current SilverStripeNavigator (SilverStripeNavigatorItems).
* Navigator is supplied by the backend and contains all state options for the current object.
*/
_getNavigatorStates: function() {
// Walk through available states and get the URLs.
var urlMap = $.map(this.getAllowedStates(), function(name) {
var stateLink = $('.cms-preview-states .state-name[data-name=' + name + ']');
if(stateLink.length) {
return {
name: name,
url: stateLink.attr('data-link'),
active: stateLink.is(':radio') ? stateLink.is(':checked') : stateLink.is(':selected')
};
} else {
return null;
}
});
return urlMap;
},
/**
* Load current state into the preview (e.g. StageLink or LiveLink).
* We try to reuse the state we have been previously in. Otherwise we fall back
* to the first state available on the "AllowedStates" list.
*
* @returns New state name.
*/
_loadCurrentState: function() {
if (!this.getIsPreviewEnabled()) return this;
var states = this._getNavigatorStates();
var currentStateName = this.getCurrentStateName();
var currentState = null;
// Find current state within currently available states.
if (states) {
currentState = $.grep(states, function(state, index) {
return (
currentStateName === state.name ||
(!currentStateName && state.active)
);
});
}
var url = null;
if (currentState[0]) {
// State is available on the newly loaded content. Get it.
url = currentState[0].url;
} else if (states.length) {
// Fall back to the first available content state.
this.setCurrentStateName(states[0].name);
url = states[0].url;
} else {
// No state available at all.
this.setCurrentStateName(null);
}
// Mark url as a preview url so it can get special treatment
url += ((url.indexOf('?') === -1) ? '?' : '&') + 'CMSPreview=1';
// If this preview panel isn't visible at the moment, delay loading the URL until it (maybe) is later
if (this.is('.column-hidden')) {
this.setPendingURL(url);
this._loadUrl('about:blank');
this._block();
}
else {
this.setPendingURL(null);
if (url) {
this._loadUrl(url);
this._unblock();
}
else {
this._block();
}
}
return this;
},
/**
* Move the navigator from the content to the preview bar.
*/
_moveNavigator: function() {
var previewEl = $('.cms-preview .cms-preview-controls');
var navigatorEl = $('.cms-edit-form .cms-navigator');
if (navigatorEl.length && previewEl.length) {
// Navigator is available - install the navigator.
previewEl.html($('.cms-edit-form .cms-navigator').detach());
} else {
// Navigator not available.
this._block();
}
},
/**
* Loads the matching edit form for a page viewed in the preview iframe,
* based on metadata sent along with this document.
*/
_loadCurrentPage: function() {
if (!this.getIsPreviewEnabled()) return;
var doc = this.find('iframe')[0].contentDocument,
containerEl = $('.cms-container');
// Load this page in the admin interface if appropriate
var id = $(doc).find('meta[name=x-page-id]').attr('content');
var editLink = $(doc).find('meta[name=x-cms-edit-link]').attr('content');
var contentPanel = $('.cms-content');
if(id && contentPanel.find(':input[name=ID]').val() != id) {
// Ignore behaviour without history support (as we need ajax loading
// for the new form to load in the background)
if(window.History.enabled)
$('.cms-container').entwine('.ss').loadPanel(editLink);
}
},
/**
* Prepare the iframe content for preview.
*/
_adjustIframeForPreview: function() {
var iframe = this.find('iframe')[0];
if(iframe){
var doc = iframe.contentDocument;
}else{
return;
}
if(!doc) return;
// Open external links in new window to avoid "escaping" the internal page context in the preview
// iframe, which is important to stay in for the CMS logic.
var links = doc.getElementsByTagName('A');
for (var i = 0; i < links.length; i++) {
var href = links[i].getAttribute('href');
if(!href) continue;
if (href.match(/^http:\/\//)) links[i].setAttribute('target', '_blank');
}
// Hide the navigator from the preview iframe and use only the CMS one.
var navi = doc.getElementById('SilverStripeNavigator');
if(navi) navi.style.display = 'none';
var naviMsg = doc.getElementById('SilverStripeNavigatorMessage');
if(naviMsg) naviMsg.style.display = 'none';
// Trigger extensions.
this.trigger('afterIframeAdjustedForPreview', [ doc ]);
}
});
$('.cms-edit-form').entwine({
onadd: function() {
this._super();
$('.cms-preview')._initialiseFromContent();
}
});
/**
* "Preview state" functions.
* -------------------------------------------------------------------
*/
$('.cms-preview-states').entwine({
/**
* Change the appearance of the state selector.
*/
changeVisibleState: function(state) {
this.find('input[data-name="'+state+'"]').prop('checked', true);
}
});
$('.cms-preview-states .state-name').entwine({
/**
* Reacts to the user changing the state of the preview.
*/
onclick: function(e) {
//Add and remove classes to make switch work ok in old IE
this.parent().find('.active').removeClass('active');
this.next('label').addClass('active');
var targetStateName = $(this).attr('data-name');
// Reload preview with the selected state.
$('.cms-preview').changeState(targetStateName);
}
});
/**
* "Preview mode" functions
* -------------------------------------------------------------------
*/
$('.preview-mode-selector').entwine({
/**
* Change the appearance of the mode selector.
*/
changeVisibleMode: function(mode) {
this.find('select')
.val(mode)
.trigger('liszt:updated')
._addIcon();
}
});
$('.preview-mode-selector select').entwine({
/**
* Reacts to the user changing the preview mode.
*/
onchange: function(e) {
this._super(e);
e.preventDefault();
var targetStateName = $(this).val();
$('.cms-preview').changeMode(targetStateName);
}
});
$('.preview-mode-selector .chzn-results li').entwine({
/**
* IE8 doesn't support programatic access to onchange event
* so react on click
*/
onclick:function(e){
if ($.browser.msie) {
e.preventDefault();
var index = this.index();
var targetStateName = this.closest('.preview-mode-selector').find('select option:eq('+index+')').val();
//var targetStateName = $(this).val();
$('.cms-preview').changeMode(targetStateName);
}
}
});
/**
* Adjust the visibility of the preview-mode selector in the CMS part (hidden if preview is visible).
*/
$('.cms-preview.column-hidden').entwine({
onmatch: function() {
$('#preview-mode-dropdown-in-content').show();
// Alert the user as to why the preview is hidden
if ($('.cms-preview .result-selected').hasClass('font-icon-columns')) {
statusMessage(ss.i18n._t(
'LeftAndMain.DISABLESPLITVIEW',
"Screen too small to show site preview in split mode"),
"error");
}
this._super();
},
onunmatch: function() {
$('#preview-mode-dropdown-in-content').hide();
this._super();
}
});
/**
* Initialise the preview-mode selector in the CMS part (could be hidden if preview is visible).
*/
$('#preview-mode-dropdown-in-content').entwine({
onmatch: function() {
if ($('.cms-preview').is('.column-hidden')) {
this.show();
}
else {
this.hide();
}
this._super();
},
onunmatch: function() {
this._super();
}
});
/**
* "Preview size" functions
* -------------------------------------------------------------------
*/
$('.preview-size-selector').entwine({
/**
* Change the appearance of the size selector.
*/
changeVisibleSize: function(size) {
this.find('select')
.val(size)
.trigger('liszt:updated')
._addIcon();
}
});
$('.preview-size-selector select').entwine({
/**
* Trigger change in the preview size.
*/
onchange: function(e) {
e.preventDefault();
var targetSizeName = $(this).val();
$('.cms-preview').changeSize(targetSizeName);
}
});
/**
* "Chosen" plumbing.
* -------------------------------------------------------------------
*/
/*
* Add a class to the chzn select trigger based on the currently
* selected option. Update as this changes
*/
$('.preview-selector select.preview-dropdown').entwine({
'onliszt:showing_dropdown': function() {
this.siblings().find('.chzn-drop').addClass('open')._alignRight();
},
'onliszt:hiding_dropdown': function() {
this.siblings().find('.chzn-drop').removeClass('open')._removeRightAlign();
},
/**
* Trigger additional initial icon update when the control is fully loaded.
* Solves an IE8 timing issue.
*/
'onliszt:ready': function() {
this._super();
this._addIcon();
},
_addIcon: function(){
var selected = this.find(':selected');
var iconClass = selected.attr('data-icon');
var target = this.parent().find('.chzn-container a.chzn-single');
var oldIcon = target.attr('data-icon');
if(typeof oldIcon !== 'undefined'){
target.removeClass(oldIcon);
}
target.addClass(iconClass);
target.attr('data-icon', iconClass);
return this;
}
});
$('.preview-selector .chzn-drop').entwine({
_alignRight: function(){
var that = this;
$(this).hide();
/* Delay so styles applied after chosen applies css
(the line after we find out the dropdown is open)
*/
setTimeout(function(){
$(that).css({left:'auto', right:0});
$(that).show();
}, 100);
},
_removeRightAlign:function(){
$(this).css({right:'auto'});
}
});
/*
* Means of having extra styled data in chzn 'preview-selector' selects
* When chzn ul is ready, grab data-description from original select.
* If it exists, append to option and add description class to list item
*/
/*
Currently buggy (adds dexcription, then re-renders). This may need to
be done inside chosen. Chosen recommends to do this stuff in the css,
but that option is inaccessible and untranslatable
(https://github.com/harvesthq/chosen/issues/399)
$('.preview-selector .chzn-drop ul').entwine({
onmatch: function() {
this.extraData();
this._super();
},
onunmatch: function() {
this._super();
},
extraData: function(){
var that = this;
var options = this.closest('.preview-selector').find('select option');
$.each(options, function(index, option){
var target = $(that).find("li:eq(" + index + ")");
var description = $(option).attr('data-description');
if(description != undefined && !$(target).hasClass('description')){
$(target).append('<span>' + description + '</span>');
$(target).addClass('description');
}
});
}
}); */
$('.preview-mode-selector .chzn-drop li:last-child').entwine({
onmatch: function () {
if ($('.preview-mode-selector').hasClass('split-disabled')) {
this.parent().append('<div class="disabled-tooltip"></div>');
} else {
this.parent().append('<div class="disabled-tooltip" style="display: none;"></div>');
}
}
});
/**
* Recalculate the preview space to allow for horizontal scrollbar and the preview actions panel
*/
$('.preview-scroll').entwine({
/**
* Height of the preview actions panel
*/
ToolbarSize: 53,
_redraw: function() {
var toolbarSize = this.getToolbarSize();
if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
var previewHeight = (this.height() - toolbarSize);
this.height(previewHeight);
},
onmatch: function() {
this._redraw();
this._super();
},
onunmatch: function() {
this._super();
}
// TODO: Need to recalculate on resize of browser
});
/**
* Rotate preview to landscape
*/
$('.preview-device-outer').entwine({
onclick: function () {
this.toggleClass('rotate');
}
});
});
}(jQuery));

View File

@ -1,496 +0,0 @@
/**
* File: LeftAndMain.Tree.js
*/
(function($) {
$.entwine('ss.tree', function($){
$('.cms-tree').entwine({
Hints: null,
IsUpdatingTree: false,
IsLoaded: false,
onadd: function(){
this._super();
// Don't reapply (expensive) tree behaviour if already present
if($.isNumeric(this.data('jstree_instance_id'))) return;
var hints = this.attr('data-hints');
if(hints) this.setHints($.parseJSON(hints));
/**
* @todo Icon and page type hover support
* @todo Sorting of sub nodes (originally placed in context menu)
* @todo Automatic load of full subtree via ajax on node checkbox selection (minNodeCount = 0)
* to avoid doing partial selection with "hidden nodes" (unloaded markup)
* @todo Disallow drag'n'drop when node has "noChildren" set (see siteTreeHints)
* @todo Disallow moving of pages marked as deleted
* most likely by server response codes rather than clientside
* @todo "defaultChild" when creating a page (sitetreeHints)
* @todo Duplicate page (originally located in context menu)
* @todo Update tree node title information and modified state after reordering (response is a JSON array)
*
* Tasks most likely not required after moving to a standalone tree:
*
* @todo Context menu - to be replaced by a bezel UI
* @todo Refresh form for selected tree node if affected by reordering (new parent relationship)
* @todo Cancel current form load via ajax when new load is requested (synchronous loading)
*/
var self = this;
this
.jstree(this.getTreeConfig())
.bind('loaded.jstree', function(e, data) {
self.setIsLoaded(true);
// Add ajax settings after init period to avoid unnecessary initial ajax load
// of existing tree in DOM - see load_node_html()
data.inst._set_settings({'html_data': {'ajax': {
'url': self.data('urlTree'),
'data': function(node) {
var params = self.data('searchparams') || [];
// Avoid duplication of parameters
params = $.grep(params, function(n, i) {return (n.name != 'ID' && n.name != 'value');});
params.push({name: 'ID', value: $(node).data("id") ? $(node).data("id") : 0});
params.push({name: 'ajax', value: 1});
return params;
}
}}});
self.updateFromEditForm();
self.css('visibility', 'visible');
// Only show checkboxes with .multiple class
data.inst.hide_checkboxes();
})
.bind('before.jstree', function(e, data) {
if(data.func == 'start_drag') {
// Don't allow drag'n'drop if multi-select is enabled'
if(!self.hasClass('draggable') || self.hasClass('multiselect')) {
e.stopImmediatePropagation();
return false;
}
}
if($.inArray(data.func, ['check_node', 'uncheck_node'])) {
// don't allow check and uncheck if parent is disabled
var node = $(data.args[0]).parents('li:first');
var allowedChildren = node.find('li:not(.disabled)');
// if there are child nodes that aren't disabled, allow expanding the tree
if(node.hasClass('disabled') && allowedChildren == 0) {
e.stopImmediatePropagation();
return false;
}
}
})
.bind('move_node.jstree', function(e, data) {
if(self.getIsUpdatingTree()) return;
var movedNode = data.rslt.o, newParentNode = data.rslt.np, oldParentNode = data.inst._get_parent(movedNode), newParentID = $(newParentNode).data('id') || 0, nodeID = $(movedNode).data('id');
var siblingIDs = $.map($(movedNode).siblings().andSelf(), function(el) {
return $(el).data('id');
});
$.ajax({
'url': self.data('urlSavetreenode'),
'type': 'POST',
'data': {
ID: nodeID,
ParentID: newParentID,
SiblingIDs: siblingIDs
},
success: function() {
// We only need to update the ParentID if the current page we're on is the page being moved
if ($('.cms-edit-form :input[name=ID]').val() == nodeID) {
$('.cms-edit-form :input[name=ParentID]').val(newParentID);
}
self.updateNodesFromServer([nodeID]);
},
statusCode: {
403: function() {
$.jstree.rollback(data.rlbk);
}
}
});
})
// Make some jstree events delegatable
.bind('select_node.jstree check_node.jstree uncheck_node.jstree', function(e, data) {
$(document).triggerHandler(e, data);
});
},
onremove: function(){
this.jstree('destroy');
this._super();
},
'from .cms-container': {
onafterstatechange: function(e){
this.updateFromEditForm();
// No need to refresh tree nodes, we assume only form submits cause state changes
}
},
'from .cms-container form': {
onaftersubmitform: function(e){
var id = $('.cms-edit-form :input[name=ID]').val();
// TODO Trigger by implementing and inspecting "changed records" metadata
// sent by form submission response (as HTTP response headers)
this.updateNodesFromServer([id]);
}
},
getTreeConfig: function() {
var self = this;
return {
'core': {
'initially_open': ['record-0'],
'animation': 0,
'html_titles': true
},
'html_data': {
// 'ajax' will be set on 'loaded.jstree' event
},
'ui': {
"select_limit" : 1,
'initially_select': [this.find('.current').attr('id')]
},
"crrm": {
'move': {
// Check if a node is allowed to be moved.
// Caution: Runs on every drag over a new node
'check_move': function(data) {
var movedNode = $(data.o), newParent = $(data.np),
isMovedOntoContainer = data.ot.get_container()[0] == data.np[0],
movedNodeClass = movedNode.getClassname(),
newParentClass = newParent.getClassname(),
// Check allowedChildren of newParent or against root node rules
hints = self.getHints(),
disallowedChildren = [],
hintKey = newParentClass ? newParentClass : 'Root',
hint = (hints && typeof hints[hintKey] != 'undefined') ? hints[hintKey] : null;
// Special case for VirtualPage: Check that original page type is an allowed child
if(hint && movedNode.attr('class').match(/VirtualPage-([^\s]*)/)) movedNodeClass = RegExp.$1;
if(hint) disallowedChildren = (typeof hint.disallowedChildren != 'undefined') ? hint.disallowedChildren : [];
var isAllowed = (
// Don't allow moving the root node
movedNode.data('id') !== 0
// Archived pages can't be moved
&& !movedNode.hasClass('status-archived')
// Only allow moving node inside the root container, not before/after it
&& (!isMovedOntoContainer || data.p == 'inside')
// Children are generally allowed on parent
&& !newParent.hasClass('nochildren')
// movedNode is allowed as a child
&& (!disallowedChildren.length || $.inArray(movedNodeClass, disallowedChildren) == -1)
);
return isAllowed;
}
}
},
'dnd': {
"drop_target" : false,
"drag_target" : false
},
'checkbox': {
'two_state': true
},
'themes': {
'theme': 'apple',
'url': $('body').data('frameworkpath') + '/thirdparty/jstree/themes/apple/style.css'
},
// Caution: SilverStripe has disabled $.vakata.css.add_sheet() for performance reasons,
// which means you need to add any CSS manually to framework/admin/scss/_tree.css
'plugins': [
'html_data', 'ui', 'dnd', 'crrm', 'themes',
'checkbox' // checkboxes are hidden unless .multiple is set
]
};
},
/**
* Function:
* search
*
* Parameters:
* (Object) data Pass empty data to cancel search
* (Function) callback Success callback
*/
search: function(params, callback) {
if(params) this.data('searchparams', params);
else this.removeData('searchparams');
this.jstree('refresh', -1, callback);
},
/**
* Function: getNodeByID
*
* Parameters:
* (Int) id
*
* Returns
* DOMElement
*/
getNodeByID: function(id) {
return this.find('*[data-id='+id+']');
},
/**
* Creates a new node from the given HTML.
* Wrapping around jstree API because we want the flexibility to define
* the node's <li> ourselves. Places the node in the tree
* according to data.ParentID.
*
* Parameters:
* (String) HTML New node content (<li>)
* (Object) Map of additional data, e.g. ParentID
* (Function) Success callback
*/
createNode: function(html, data, callback) {
var self = this,
parentNode = data.ParentID !== void 0 ? self.getNodeByID(data.ParentID) : false, // Explicitly check for undefined as 0 is a valid ParentID
newNode = $(html);
// Extract the state for the new node from the properties taken from the provided HTML template.
// This will correctly initialise the behaviour of the node for ajax loading of children.
var properties = {data: ''};
if(newNode.hasClass('jstree-open')) {
properties.state = 'open';
} else if(newNode.hasClass('jstree-closed')) {
properties.state = 'closed';
}
this.jstree(
'create_node',
parentNode.length ? parentNode : -1,
'last',
properties,
function(node) {
var origClasses = node.attr('class');
// Copy attributes
for(var i=0; i<newNode[0].attributes.length; i++){
var attr = newNode[0].attributes[i];
node.attr(attr.name, attr.value);
}
// Substitute html from request for that generated by jstree
node.addClass(origClasses).html(newNode.html());
callback(node);
}
);
},
/**
* Updates a node's state in the tree,
* including all of its HTML, as well as its position.
*
* Parameters:
* (DOMElement) Existing node
* (String) HTML New node content (<li>)
* (Object) Map of additional data, e.g. ParentID
*/
updateNode: function(node, html, data) {
var self = this, newNode = $(html), origClasses = node.attr('class');
var nextNode = data.NextID ? this.getNodeByID(data.NextID) : false;
var prevNode = data.PrevID ? this.getNodeByID(data.PrevID) : false;
var parentNode = data.ParentID ? this.getNodeByID(data.ParentID) : false;
// Copy attributes. We can't replace the node completely
// without removing or detaching its children nodes.
$.each(['id', 'style', 'class', 'data-pagetype'], function(i, attrName) {
node.attr(attrName, newNode.attr(attrName));
});
// To avoid conflicting classes when the node gets its content replaced (see below)
// Filter out all previous status flags if they are not in the class property of the new node
origClasses = origClasses.replace(/status-[^\s]*/, '');
// Replace inner content
var origChildren = node.children('ul').detach();
node.addClass(origClasses).html(newNode.html()).append(origChildren);
if (nextNode && nextNode.length) {
this.jstree('move_node', node, nextNode, 'before');
}
else if (prevNode && prevNode.length) {
this.jstree('move_node', node, prevNode, 'after');
}
else {
this.jstree('move_node', node, parentNode.length ? parentNode : -1);
}
},
/**
* Sets the current state based on the form the tree is managing.
*/
updateFromEditForm: function() {
var node, id = $('.cms-edit-form :input[name=ID]').val();
if(id) {
node = this.getNodeByID(id);
if(node.length) {
this.jstree('deselect_all');
this.jstree('select_node', node);
} else {
// If form is showing an ID that doesn't exist in the tree,
// get it from the server
this.updateNodesFromServer([id]);
}
} else {
// If no ID exists in a form view, we're displaying the tree on its own,
// hence to page should show as active
this.jstree('deselect_all');
}
},
/**
* Reloads the view of one or more tree nodes
* from the server, ensuring that their state is up to date
* (icon, title, hierarchy, badges, etc).
* This is easier, more consistent and more extensible
* than trying to correct all aspects via DOM modifications,
* based on the sparse data available in the current edit form.
*
* Parameters:
* (Array) List of IDs to retrieve
*/
updateNodesFromServer: function(ids) {
if(this.getIsUpdatingTree() || !this.getIsLoaded()) return;
var self = this, i, includesNewNode = false;
this.setIsUpdatingTree(true);
self.jstree('save_selected');
var correctStateFn = function(node) {
// Duplicates can be caused by the subtree reloading through
// a tree "open"/"select" event, while at the same time creating a new node
self.getNodeByID(node.data('id')).not(node).remove();
// Select this node
self.jstree('deselect_all');
self.jstree('select_node', node);
};
// TODO 'initially_opened' config doesn't apply here
self.jstree('open_node', this.getNodeByID(0));
self.jstree('save_opened');
self.jstree('save_selected');
$.ajax({
url: $.path.addSearchParams(this.data('urlUpdatetreenodes'), 'ids=' + ids.join(',')),
dataType: 'json',
success: function(data, xhr) {
$.each(data, function(nodeId, nodeData) {
var node = self.getNodeByID(nodeId);
// If no node data is given, assume the node has been removed
if(!nodeData) {
self.jstree('delete_node', node);
return;
}
// Check if node exists, create if necessary
if(node.length) {
self.updateNode(node, nodeData.html, nodeData);
setTimeout(function() {
correctStateFn(node);
}, 500);
} else {
includesNewNode = true;
// If the parent node can't be found, it might have not been loaded yet.
// This can happen for deep trees which require ajax loading.
// Assumes that the new node has been submitted to the server already.
if(nodeData.ParentID && !self.find('li[data-id='+nodeData.ParentID+']').length) {
self.jstree('load_node', -1, function() {
newNode = self.find('li[data-id='+nodeId+']');
correctStateFn(newNode);
});
} else {
self.createNode(nodeData.html, nodeData, function(newNode) {
correctStateFn(newNode);
});
}
}
});
if(!includesNewNode) {
self.jstree('deselect_all');
self.jstree('reselect');
self.jstree('reopen');
}
},
complete: function() {
self.setIsUpdatingTree(false);
}
});
}
});
$('.cms-tree.multiple').entwine({
onmatch: function() {
this._super();
this.jstree('show_checkboxes');
},
onunmatch: function() {
this._super();
this.jstree('uncheck_all');
this.jstree('hide_checkboxes');
},
/**
* Function: getSelectedIDs
*
* Returns:
* (Array)
*/
getSelectedIDs: function() {
return $(this)
.jstree('get_checked')
.not('.disabled')
.map(function() {
return $(this).data('id');
})
.get();
}
});
$('.cms-tree li').entwine({
/**
* Function: setEnabled
*
* Parameters:
* (bool)
*/
setEnabled: function(bool) {
this.toggleClass('disabled', !(bool));
},
/**
* Function: getClassname
*
* Returns PHP class for this element. Useful to check business rules like valid drag'n'drop targets.
*/
getClassname: function() {
var matches = this.attr('class').match(/class-([^\s]*)/i);
return matches ? matches[1] : '';
},
/**
* Function: getID
*
* Returns:
* (Number)
*/
getID: function() {
return this.data('id');
}
});
});
}(jQuery));

View File

@ -1,1516 +0,0 @@
jQuery.noConflict();
/**
* File: LeftAndMain.js
*/
(function($) {
window.ss = window.ss || {};
var windowWidth, windowHeight;
/**
* @func debounce
* @param func {function} - The callback to invoke after `wait` milliseconds.
* @param wait {number} - Milliseconds to wait.
* @param immediate {boolean} - If true the callback will be invoked at the start rather than the end.
* @return {function}
* @desc Returns a function that will not be called until it hasn't been invoked for `wait` seconds.
*/
window.ss.debounce = function (func, wait, immediate) {
var timeout, context, args;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
return function() {
var callNow = immediate && !timeout;
context = this;
args = arguments;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
};
$(window).bind('resize.leftandmain', function(e) {
// Entwine's 'fromWindow::onresize' does not trigger on IE8. Use synthetic event.
var cb = function() {$('.cms-container').trigger('windowresize');};
// Workaround to avoid IE8 infinite loops when elements are resized as a result of this event
if($.browser.msie && parseInt($.browser.version, 10) < 9) {
var newWindowWidth = $(window).width(), newWindowHeight = $(window).height();
if(newWindowWidth != windowWidth || newWindowHeight != windowHeight) {
windowWidth = newWindowWidth;
windowHeight = newWindowHeight;
cb();
}
} else {
cb();
}
});
// setup jquery.entwine
$.entwine.warningLevel = $.entwine.WARN_LEVEL_BESTPRACTISE;
$.entwine('ss', function($) {
/*
* Handle messages sent via nested iframes
* Messages should be raised via postMessage with an object with the 'type' parameter given.
* An optional 'target' and 'data' parameter can also be specified. If no target is specified
* events will be sent to the window instead.
* type should be one of:
* - 'event' - Will trigger the given event (specified by 'event') on the target
* - 'callback' - Will call the given method (specified by 'callback') on the target
*/
$(window).on("message", function(e) {
var target,
event = e.originalEvent,
data = JSON.parse(event.data);
// Reject messages outside of the same origin
if($.path.parseUrl(window.location.href).domain !== $.path.parseUrl(event.origin).domain) return;
// Get target of this action
target = typeof(data.target) === 'undefined'
? $(window)
: $(data.target);
// Determine action
switch(data.type) {
case 'event':
target.trigger(data.event, data.data);
break;
case 'callback':
target[data.callback].call(target, data.data);
break;
}
});
/**
* Position the loading spinner animation below the ss logo
*/
var positionLoadingSpinner = function() {
var offset = 120; // offset from the ss logo
var spinner = $('.ss-loading-screen .loading-animation');
var top = ($(window).height() - spinner.height()) / 2;
spinner.css('top', top + offset);
spinner.show();
};
// apply an select element only when it is ready, ie. when it is rendered into a template
// with css applied and got a width value.
var applyChosen = function(el) {
if(el.is(':visible')) {
el.addClass('has-chzn').chosen({
allow_single_deselect: true,
disable_search_threshold: 20
});
var title = el.prop('title');
if(title) {
el.siblings('.chzn-container').prop('title', title);
}
} else {
setTimeout(function() {
// Make sure it's visible before applying the ui
el.show();
applyChosen(el); },
500);
}
};
/**
* Compare URLs, but normalize trailing slashes in
* URL to work around routing weirdnesses in SS_HTTPRequest.
* Also normalizes relative URLs by prefixing them with the <base>.
*/
var isSameUrl = function(url1, url2) {
var baseUrl = $('base').attr('href');
url1 = $.path.isAbsoluteUrl(url1) ? url1 : $.path.makeUrlAbsolute(url1, baseUrl),
url2 = $.path.isAbsoluteUrl(url2) ? url2 : $.path.makeUrlAbsolute(url2, baseUrl);
var url1parts = $.path.parseUrl(url1), url2parts = $.path.parseUrl(url2);
return (
url1parts.pathname.replace(/\/*$/, '') == url2parts.pathname.replace(/\/*$/, '') &&
url1parts.search == url2parts.search
);
};
var ajaxCompleteEvent = window.ss.debounce(function () {
$(window).trigger('ajaxComplete');
}, 1000, true);
$(window).bind('resize', positionLoadingSpinner).trigger('resize');
// global ajax handlers
$(document).ajaxComplete(function(e, xhr, settings) {
// Simulates a redirect on an ajax response.
if(window.History.enabled) {
var url = xhr.getResponseHeader('X-ControllerURL'),
// TODO Replaces trailing slashes added by History after locale (e.g. admin/?locale=en/)
origUrl = History.getPageUrl().replace(/\/$/, ''),
destUrl = settings.url,
opts;
// Only redirect if controller url differs to the requested or current one
if(url !== null &&
(!isSameUrl(origUrl, url) || !isSameUrl(destUrl, url))
) {
opts = {
// Ensure that redirections are followed through by history API by handing it a unique ID
id: (new Date()).getTime() + String(Math.random()).replace(/\D/g,''),
pjax: xhr.getResponseHeader('X-Pjax')
? xhr.getResponseHeader('X-Pjax')
: settings.headers['X-Pjax']
};
window.History.pushState(opts, '', url);
}
}
// Handle custom status message headers
var msg = (xhr.getResponseHeader('X-Status')) ? xhr.getResponseHeader('X-Status') : xhr.statusText,
reathenticate = xhr.getResponseHeader('X-Reauthenticate'),
msgType = (xhr.status < 200 || xhr.status > 399) ? 'bad' : 'good',
ignoredMessages = ['OK'];
// Enable reauthenticate dialog if requested
if(reathenticate) {
$('.cms-container').showLoginDialog();
return;
}
// Show message (but ignore aborted requests)
if(xhr.status !== 0 && msg && $.inArray(msg, ignoredMessages)) {
// Decode into UTF-8, HTTP headers don't allow multibyte
statusMessage(decodeURIComponent(msg), msgType);
}
ajaxCompleteEvent(this);
});
/**
* Main LeftAndMain interface with some control panel and an edit form.
*
* Events:
* ajaxsubmit - ...
* validate - ...
* aftersubmitform - ...
*/
$('.cms-container').entwine({
/**
* Tracks current panel request.
*/
StateChangeXHR: null,
/**
* Tracks current fragment-only parallel PJAX requests.
*/
FragmentXHR: {},
StateChangeCount: 0,
/**
* Options for the threeColumnCompressor layout algorithm.
*
* See LeftAndMain.Layout.js for description of these options.
*/
LayoutOptions: {
minContentWidth: 940,
minPreviewWidth: 400,
mode: 'content'
},
/**
* Constructor: onmatch
*/
onadd: function() {
var self = this;
// Browser detection
if($.browser.msie && parseInt($.browser.version, 10) < 8) {
$('.ss-loading-screen').append(
'<p class="ss-loading-incompat-warning"><span class="notice">' +
'Your browser is not compatible with the CMS interface. Please use Internet Explorer 8+, Google Chrome or Mozilla Firefox.' +
'</span></p>'
).css('z-index', $('.ss-loading-screen').css('z-index')+1);
$('.loading-animation').remove();
this._super();
return;
}
// Initialize layouts
this.redraw();
// Remove loading screen
$('.ss-loading-screen').hide();
$('body').removeClass('loading');
$(window).unbind('resize', positionLoadingSpinner);
this.restoreTabState();
this._super();
},
fromWindow: {
onstatechange: function(e){
this.handleStateChange(e);
}
},
'onwindowresize': function() {
this.redraw();
},
'from .cms-panel': {
ontoggle: function(){ this.redraw(); }
},
'from .cms-container': {
onaftersubmitform: function(){ this.redraw(); }
},
/**
* Ensure the user can see the requested section - restore the default view.
*/
'from .cms-menu-list li a': {
onclick: function(e) {
var href = $(e.target).attr('href');
if(e.which > 1 || href == this._tabStateUrl()) return;
this.splitViewMode();
}
},
/**
* Change the options of the threeColumnCompressor layout, and trigger layouting if needed.
* You can provide any or all options. The remaining options will not be changed.
*/
updateLayoutOptions: function(newSpec) {
var spec = this.getLayoutOptions();
var dirty = false;
for (var k in newSpec) {
if (spec[k] !== newSpec[k]) {
spec[k] = newSpec[k];
dirty = true;
}
}
if (dirty) this.redraw();
},
/**
* Enable the split view - with content on the left and preview on the right.
*/
splitViewMode: function() {
this.updateLayoutOptions({
mode: 'split'
});
},
/**
* Content only.
*/
contentViewMode: function() {
this.updateLayoutOptions({
mode: 'content'
});
},
/**
* Preview only.
*/
previewMode: function() {
this.updateLayoutOptions({
mode: 'preview'
});
},
RedrawSuppression: false,
redraw: function() {
if (this.getRedrawSuppression()) return;
if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
// Reset the algorithm.
this.data('jlayout', jLayout.threeColumnCompressor(
{
menu: this.children('.cms-menu'),
content: this.children('.cms-content'),
preview: this.children('.cms-preview')
},
this.getLayoutOptions()
));
// Trigger layout algorithm once at the top. This also lays out children - we move from outside to
// inside, resizing to fit the parent.
this.layout();
// Redraw on all the children that need it
this.find('.cms-panel-layout').redraw();
this.find('.cms-content-fields[data-layout-type]').redraw();
this.find('.cms-edit-form[data-layout-type]').redraw();
this.find('.cms-preview').redraw();
this.find('.cms-content').redraw();
},
/**
* Confirm whether the current user can navigate away from this page
*
* @param {array} selectors Optional list of selectors
* @returns {boolean} True if the navigation can proceed
*/
checkCanNavigate: function(selectors) {
// Check change tracking (can't use events as we need a way to cancel the current state change)
var contentEls = this._findFragments(selectors || ['Content']),
trackedEls = contentEls
.find(':data(changetracker)')
.add(contentEls.filter(':data(changetracker)')),
safe = true;
if(!trackedEls.length) {
return true;
}
trackedEls.each(function() {
// See LeftAndMain.EditForm.js
if(!$(this).confirmUnsavedChanges()) {
safe = false;
}
});
return safe;
},
/**
* Proxy around History.pushState() which handles non-HTML5 fallbacks,
* as well as global change tracking. Change tracking needs to be synchronous rather than event/callback
* based because the user needs to be able to abort the action completely.
*
* See handleStateChange() for more details.
*
* Parameters:
* - {String} url
* - {String} title New window title
* - {Object} data Any additional data passed through to History.pushState()
* - {boolean} forceReload Forces the replacement of the current history state, even if the URL is the same, i.e. allows reloading.
*/
loadPanel: function(url, title, data, forceReload, forceReferer) {
if(!data) data = {};
if(!title) title = "";
if (!forceReferer) forceReferer = History.getState().url;
// Check for unsaved changes
if(!this.checkCanNavigate(data.pjax ? data.pjax.split(',') : ['Content'])) {
return;
}
// Save tab selections so we can restore them later
this.saveTabState();
if(window.History.enabled) {
$.extend(data, {__forceReferer: forceReferer});
// Active menu item is set based on X-Controller ajax header,
// which matches one class on the menu
if(forceReload) {
// Add a parameter to make sure the page gets reloaded even if the URL is the same.
$.extend(data, {__forceReload: Math.random()});
window.History.replaceState(data, title, url);
} else {
window.History.pushState(data, title, url);
}
} else {
window.location = $.path.makeUrlAbsolute(url, $('base').attr('href'));
}
},
/**
* Nice wrapper for reloading current history state.
*/
reloadCurrentPanel: function() {
this.loadPanel(window.History.getState().url, null, null, true);
},
/**
* Function: submitForm
*
* Parameters:
* {DOMElement} form - The form to be submitted. Needs to be passed
* in to avoid entwine methods/context being removed through replacing the node itself.
* {DOMElement} button - The pressed button (optional)
* {Function} callback - Called in complete() handler of jQuery.ajax()
* {Object} ajaxOptions - Object literal to merge into $.ajax() call
*
* Returns:
* (boolean)
*/
submitForm: function(form, button, callback, ajaxOptions) {
var self = this;
// look for save button
if(!button) button = this.find('.Actions :submit[name=action_save]');
// default to first button if none given - simulates browser behaviour
if(!button) button = this.find('.Actions :submit:first');
form.trigger('beforesubmitform');
this.trigger('submitform', {form: form, button: button});
// set button to "submitting" state
$(button).addClass('loading');
// validate if required
var validationResult = form.validate();
if(typeof validationResult!=='undefined' && !validationResult) {
// TODO Automatically switch to the tab/position of the first error
statusMessage("Validation failed.", "bad");
$(button).removeClass('loading');
return false;
}
// get all data from the form
var formData = form.serializeArray();
// add button action
formData.push({name: $(button).attr('name'), value:'1'});
// Artificial HTTP referer, IE doesn't submit them via ajax.
// Also rewrites anchors to their page counterparts, which is important
// as automatic browser ajax response redirects seem to discard the hash/fragment.
// TODO Replaces trailing slashes added by History after locale (e.g. admin/?locale=en/)
formData.push({name: 'BackURL', value:History.getPageUrl().replace(/\/$/, '')});
// Save tab selections so we can restore them later
this.saveTabState();
// Standard Pjax behaviour is to replace the submitted form with new content.
// The returned view isn't always decided upon when the request
// is fired, so the server might decide to change it based on its own logic,
// sending back different `X-Pjax` headers and content
jQuery.ajax(jQuery.extend({
headers: {"X-Pjax" : "CurrentForm,Breadcrumbs"},
url: form.attr('action'),
data: formData,
type: 'POST',
complete: function() {
$(button).removeClass('loading');
},
success: function(data, status, xhr) {
form.removeClass('changed'); // TODO This should be using the plugin API
if(callback) callback(data, status, xhr);
var newContentEls = self.handleAjaxResponse(data, status, xhr);
if(!newContentEls) return;
newContentEls.filter('form').trigger('aftersubmitform', {status: status, xhr: xhr, formData: formData});
}
}, ajaxOptions));
return false;
},
/**
* Last html5 history state
*/
LastState: null,
/**
* Flag to pause handleStateChange
*/
PauseState: false,
/**
* Handles ajax loading of new panels through the window.History object.
* To trigger loading, pass a new URL to window.History.pushState().
* Use loadPanel() as a pushState() wrapper as it provides some additional functionality
* like global changetracking and user aborts.
*
* Due to the nature of history management, no callbacks are allowed.
* Use the 'beforestatechange' and 'afterstatechange' events instead,
* or overwrite the beforeLoad() and afterLoad() methods on the
* DOM element you're loading the new content into.
* Although you can pass data into pushState(), it shouldn't contain
* DOM elements or callback closures.
*
* The passed URL should allow reconstructing important interface state
* without additional parameters, in the following use cases:
* - Explicit loading through History.pushState()
* - Implicit loading through browser navigation event triggered by the user (forward or back)
* - Full window refresh without ajax
* For example, a ModelAdmin search event should contain the search terms
* as URL parameters, and the result display should automatically appear
* if the URL is loaded without ajax.
*/
handleStateChange: function() {
if(this.getPauseState()) {
return;
}
// Don't allow parallel loading to avoid edge cases
if(this.getStateChangeXHR()) this.getStateChangeXHR().abort();
var self = this, h = window.History, state = h.getState(),
fragments = state.data.pjax || 'Content', headers = {},
fragmentsArr = fragments.split(','),
contentEls = this._findFragments(fragmentsArr);
// For legacy IE versions (IE7 and IE8), reload without ajax
// as a crude way to fix memory leaks through whole window refreshes.
this.setStateChangeCount(this.getStateChangeCount() + 1);
var isLegacyIE = ($.browser.msie && parseInt($.browser.version, 10) < 9);
if(isLegacyIE && this.getStateChangeCount() > 20) {
document.location.href = state.url;
return;
}
if(!this.checkCanNavigate()) {
// If history is emulated (ie8 or below) disable attempting to restore
if(h.emulated.pushState) {
return;
}
var lastState = this.getLastState();
// Suppress panel loading while resetting state
this.setPauseState(true);
// Restore best last state
if(lastState) {
h.pushState(lastState.id, lastState.title, lastState.url);
} else {
h.back();
}
this.setPauseState(false);
// Abort loading of this panel
return;
}
this.setLastState(state);
// If any of the requested Pjax fragments don't exist in the current view,
// fetch the "Content" view instead, which is the "outermost" fragment
// that can be reloaded without reloading the whole window.
if(contentEls.length < fragmentsArr.length) {
fragments = 'Content', fragmentsArr = ['Content'];
contentEls = this._findFragments(fragmentsArr);
}
this.trigger('beforestatechange', {state: state, element: contentEls});
// Set Pjax headers, which can declare a preference for the returned view.
// The actually returned view isn't always decided upon when the request
// is fired, so the server might decide to change it based on its own logic.
headers['X-Pjax'] = fragments;
// Set 'fake' referer - we call pushState() before making the AJAX request, so we have to
// set our own referer here
if (typeof state.data.__forceReferer !== 'undefined') {
// Ensure query string is properly encoded if present
var url = state.data.__forceReferer;
try {
// Prevent double-encoding by attempting to decode
url = decodeURI(url);
} catch(e) {
// URL not encoded, or was encoded incorrectly, so do nothing
} finally {
// Set our referer header to the encoded URL
headers['X-Backurl'] = encodeURI(url);
}
}
contentEls.addClass('loading');
var xhr = $.ajax({
headers: headers,
url: state.url,
complete: function() {
self.setStateChangeXHR(null);
// Remove loading indication from old content els (regardless of which are replaced)
contentEls.removeClass('loading');
},
success: function(data, status, xhr) {
var els = self.handleAjaxResponse(data, status, xhr, state);
self.trigger('afterstatechange', {data: data, status: status, xhr: xhr, element: els, state: state});
}
});
this.setStateChangeXHR(xhr);
},
/**
* ALternative to loadPanel/submitForm.
*
* Triggers a parallel-fetch of a PJAX fragment, which is a separate request to the
* state change requests. There could be any amount of these fetches going on in the background,
* and they don't register as a HTML5 history states.
*
* This is meant for updating a PJAX areas that are not complete panel/form reloads. These you'd
* normally do via submitForm or loadPanel which have a lot of automation built in.
*
* On receiving successful response, the framework will update the element tagged with appropriate
* data-pjax-fragment attribute (e.g. data-pjax-fragment="<pjax-fragment-name>"). Make sure this element
* is available.
*
* Example usage:
* $('.cms-container').loadFragment('admin/foobar/', 'FragmentName');
*
* @param url string Relative or absolute url of the controller.
* @param pjaxFragments string PJAX fragment(s), comma separated.
*/
loadFragment: function(url, pjaxFragments) {
var self = this,
xhr,
headers = {},
baseUrl = $('base').attr('href'),
fragmentXHR = this.getFragmentXHR();
// Make sure only one XHR for a specific fragment is currently in progress.
if(
typeof fragmentXHR[pjaxFragments]!=='undefined' &&
fragmentXHR[pjaxFragments]!==null
) {
fragmentXHR[pjaxFragments].abort();
fragmentXHR[pjaxFragments] = null;
}
url = $.path.isAbsoluteUrl(url) ? url : $.path.makeUrlAbsolute(url, baseUrl);
headers['X-Pjax'] = pjaxFragments;
xhr = $.ajax({
headers: headers,
url: url,
success: function(data, status, xhr) {
var elements = self.handleAjaxResponse(data, status, xhr, null);
// We are fully done now, make it possible for others to hook in here.
self.trigger('afterloadfragment', { data: data, status: status, xhr: xhr, elements: elements });
},
error: function(xhr, status, error) {
self.trigger('loadfragmenterror', { xhr: xhr, status: status, error: error });
},
complete: function() {
// Reset the current XHR in tracking object.
var fragmentXHR = self.getFragmentXHR();
if(
typeof fragmentXHR[pjaxFragments]!=='undefined' &&
fragmentXHR[pjaxFragments]!==null
) {
fragmentXHR[pjaxFragments] = null;
}
}
});
// Store the fragment request so we can abort later, should we get a duplicate request.
fragmentXHR[pjaxFragments] = xhr;
return xhr;
},
/**
* Handles ajax responses containing plain HTML, or mulitple
* PJAX fragments wrapped in JSON (see PjaxResponseNegotiator PHP class).
* Can be hooked into an ajax 'success' callback.
*
* Parameters:
* (Object) data
* (String) status
* (XMLHTTPRequest) xhr
* (Object) state The original history state which the request was initiated with
*/
handleAjaxResponse: function(data, status, xhr, state) {
var self = this, url, selectedTabs, guessFragment;
// Support a full reload
if(xhr.getResponseHeader('X-Reload') && xhr.getResponseHeader('X-ControllerURL')) {
var baseUrl = $('base').attr('href'),
rawURL = xhr.getResponseHeader('X-ControllerURL'),
url = $.path.isAbsoluteUrl(rawURL) ? rawURL : $.path.makeUrlAbsolute(rawURL, baseUrl);
document.location.href = url;
return;
}
// Pseudo-redirects via X-ControllerURL might return empty data, in which
// case we'll ignore the response
if(!data) return;
// Update title
var title = xhr.getResponseHeader('X-Title');
if(title) document.title = decodeURIComponent(title.replace(/\+/g, ' '));
var newFragments = {}, newContentEls;
// If content type is text/json (ignoring charset and other parameters)
if(xhr.getResponseHeader('Content-Type').match(/^((text)|(application))\/json[ \t]*;?/i)) {
newFragments = data;
} else {
// Fall back to replacing the content fragment if HTML is returned
var fragment = document.createDocumentFragment();
jQuery.clean( [ data ], document, fragment, [] );
$data = $(jQuery.merge( [], fragment.childNodes ));
// Try and guess the fragment if none is provided
// TODO: data-pjax-fragment might actually give us the fragment. For now we just check most common case
guessFragment = 'Content';
if ($data.is('form') && !$data.is('[data-pjax-fragment~=Content]')) guessFragment = 'CurrentForm';
newFragments[guessFragment] = $data;
}
this.setRedrawSuppression(true);
try {
// Replace each fragment individually
$.each(newFragments, function(newFragment, html) {
var contentEl = $('[data-pjax-fragment]').filter(function() {
return $.inArray(newFragment, $(this).data('pjaxFragment').split(' ')) != -1;
}), newContentEl = $(html);
// Add to result collection
if(newContentEls) newContentEls.add(newContentEl);
else newContentEls = newContentEl;
// Update panels
if(newContentEl.find('.cms-container').length) {
throw 'Content loaded via ajax is not allowed to contain tags matching the ".cms-container" selector to avoid infinite loops';
}
// Set loading state and store element state
var origStyle = contentEl.attr('style');
var origParent = contentEl.parent();
var origParentLayoutApplied = (typeof origParent.data('jlayout')!=='undefined');
var layoutClasses = ['east', 'west', 'center', 'north', 'south', 'column-hidden'];
var elemClasses = contentEl.attr('class');
var origLayoutClasses = [];
if(elemClasses) {
origLayoutClasses = $.grep(
elemClasses.split(' '),
function(val) { return ($.inArray(val, layoutClasses) >= 0);}
);
}
newContentEl
.removeClass(layoutClasses.join(' '))
.addClass(origLayoutClasses.join(' '));
if(origStyle) newContentEl.attr('style', origStyle);
// Allow injection of inline styles, as they're not allowed in the document body.
// Not handling this through jQuery.ondemand to avoid parsing the DOM twice.
var styles = newContentEl.find('style').detach();
if(styles.length) $(document).find('head').append(styles);
// Replace panel completely (we need to override the "layout" attribute, so can't replace the child instead)
contentEl.replaceWith(newContentEl);
// Force jlayout to rebuild internal hierarchy to point to the new elements.
// This is only necessary for elements that are at least 3 levels deep. 2nd level elements will
// be taken care of when we lay out the top level element (.cms-container).
if (!origParent.is('.cms-container') && origParentLayoutApplied) {
origParent.layout();
}
});
// Re-init tabs (in case the form tag itself is a tabset)
var newForm = newContentEls.filter('form');
if(newForm.hasClass('cms-tabset')) newForm.removeClass('cms-tabset').addClass('cms-tabset');
}
finally {
this.setRedrawSuppression(false);
}
this.redraw();
this.restoreTabState((state && typeof state.data.tabState !== 'undefined') ? state.data.tabState : null);
return newContentEls;
},
/**
*
*
* Parameters:
* - fragments {Array}
* Returns: jQuery collection
*/
_findFragments: function(fragments) {
return $('[data-pjax-fragment]').filter(function() {
// Allows for more than one fragment per node
var i, nodeFragments = $(this).data('pjaxFragment').split(' ');
for(i in fragments) {
if($.inArray(fragments[i], nodeFragments) != -1) return true;
}
return false;
});
},
/**
* Function: refresh
*
* Updates the container based on the current url
*
* Returns: void
*/
refresh: function() {
$(window).trigger('statechange');
$(this).redraw();
},
/**
* Save tab selections in order to reconstruct them later.
* Requires HTML5 sessionStorage support.
*/
saveTabState: function() {
if(typeof(window.sessionStorage)=="undefined" || window.sessionStorage === null) return;
var selectedTabs = [], url = this._tabStateUrl();
this.find('.cms-tabset,.ss-tabset').each(function(i, el) {
var id = $(el).attr('id');
if(!id) return; // we need a unique reference
if(!$(el).data('tabs')) return; // don't act on uninit'ed controls
// Allow opt-out via data element or entwine property.
if($(el).data('ignoreTabState') || $(el).getIgnoreTabState()) return;
selectedTabs.push({id:id, selected:$(el).tabs('option', 'selected')});
});
if(selectedTabs) {
var tabsUrl = 'tabs-' + url;
try {
window.sessionStorage.setItem(tabsUrl, JSON.stringify(selectedTabs));
} catch(err) {
if (err.code === DOMException.QUOTA_EXCEEDED_ERR && window.sessionStorage.length === 0) {
// If this fails we ignore the error as the only issue is that it
// does not remember the tab state.
// This is a Safari bug which happens when private browsing is enabled.
return;
} else {
throw err;
}
}
}
},
/**
* Re-select previously saved tabs.
* Requires HTML5 sessionStorage support.
*
* Parameters:
* (Object) Map of tab container selectors to tab selectors.
* Used to mark a specific tab as active regardless of the previously saved options.
*/
restoreTabState: function(overrideStates) {
var self = this, url = this._tabStateUrl(),
hasSessionStorage = (typeof(window.sessionStorage)!=="undefined" && window.sessionStorage),
sessionData = hasSessionStorage ? window.sessionStorage.getItem('tabs-' + url) : null,
sessionStates = sessionData ? JSON.parse(sessionData) : false;
this.find('.cms-tabset, .ss-tabset').each(function() {
var index, tabset = $(this), tabsetId = tabset.attr('id'), tab,
forcedTab = tabset.find('.ss-tabs-force-active');
if(!tabset.data('tabs')){
return; // don't act on uninit'ed controls
}
// The tabs may have changed, notify the widget that it should update its internal state.
tabset.tabs('refresh');
// Make sure the intended tab is selected.
if(forcedTab.length) {
index = forcedTab.index();
} else if(overrideStates && overrideStates[tabsetId]) {
tab = tabset.find(overrideStates[tabsetId].tabSelector);
if(tab.length){
index = tab.index();
}
} else if(sessionStates) {
$.each(sessionStates, function(i, sessionState) {
if(tabset.is('#' + sessionState.id)){
index = sessionState.selected;
}
});
}
if(index !== null){
tabset.tabs('option', 'active', index);
self.trigger('tabstaterestored');
}
});
},
/**
* Remove any previously saved state.
*
* Parameters:
* (String) url Optional (sanitized) URL to clear a specific state.
*/
clearTabState: function(url) {
if(typeof(window.sessionStorage)=="undefined") return;
var s = window.sessionStorage;
if(url) {
s.removeItem('tabs-' + url);
} else {
for(var i=0;i<s.length;i++) {
if(s.key(i).match(/^tabs-/)) s.removeItem(s.key(i));
}
}
},
/**
* Remove tab state for the current URL.
*/
clearCurrentTabState: function() {
this.clearTabState(this._tabStateUrl());
},
_tabStateUrl: function() {
return History.getState().url
.replace(/\?.*/, '')
.replace(/#.*/, '')
.replace($('base').attr('href'), '');
},
showLoginDialog: function() {
var tempid = $('body').data('member-tempid'),
dialog = $('.leftandmain-logindialog'),
url = 'CMSSecurity/login';
// Force regeneration of any existing dialog
if(dialog.length) dialog.remove();
// Join url params
url = $.path.addSearchParams(url, {
'tempid': tempid,
'BackURL': window.location.href
});
// Show a placeholder for instant feedback. Will be replaced with actual
// form dialog once its loaded.
dialog = $('<div class="leftandmain-logindialog"></div>');
dialog.attr('id', new Date().getTime());
dialog.data('url', url);
$('body').append(dialog);
}
});
// Login dialog page
$('.leftandmain-logindialog').entwine({
onmatch: function() {
this._super();
// Create jQuery dialog
this.ssdialog({
iframeUrl: this.data('url'),
dialogClass: "leftandmain-logindialog-dialog",
autoOpen: true,
minWidth: 500,
maxWidth: 500,
minHeight: 370,
maxHeight: 400,
closeOnEscape: false,
open: function() {
$('.ui-widget-overlay').addClass('leftandmain-logindialog-overlay');
},
close: function() {
$('.ui-widget-overlay').removeClass('leftandmain-logindialog-overlay');
}
});
},
onunmatch: function() {
this._super();
},
open: function() {
this.ssdialog('open');
},
close: function() {
this.ssdialog('close');
},
toggle: function(bool) {
if(this.is(':visible')) this.close();
else this.open();
},
/**
* Callback activated by CMSSecurity_success.ss
*/
reauthenticate: function(data) {
// Replace all SecurityID fields with the given value
if(typeof(data.SecurityID) !== 'undefined') {
$(':input[name=SecurityID]').val(data.SecurityID);
}
// Update TempID for current user
if(typeof(data.TempID) !== 'undefined') {
$('body').data('member-tempid', data.TempID);
}
this.close();
}
});
/**
* Add loading overlay to selected regions in the CMS automatically.
* Not applied to all "*.loading" elements to avoid secondary regions
* like the breadcrumbs showing unnecessary loading status.
*/
$('form.loading,.cms-content.loading,.cms-content-fields.loading,.cms-content-view.loading').entwine({
onmatch: function() {
this.append('<div class="cms-content-loading-overlay ui-widget-overlay-light"></div><div class="cms-content-loading-spinner"></div>');
this._super();
},
onunmatch: function() {
this.find('.cms-content-loading-overlay,.cms-content-loading-spinner').remove();
this._super();
}
});
/** Make all buttons "hoverable" with jQuery theming. */
$('.cms input[type="submit"], .cms button, .cms input[type="reset"], .cms .ss-ui-button').entwine({
onadd: function() {
this.addClass('ss-ui-button');
if(!this.data('button')) this.button();
this._super();
},
onremove: function() {
if(this.data('button')) this.button('destroy');
this._super();
}
});
/**
* Loads the link's 'href' attribute into a panel via ajax,
* as opposed to triggering a full page reload.
* Little helper to avoid repetition, and make it easy to
* "opt in" to panel loading, while by default links still exhibit their default behaviour.
* The PJAX target can be specified via a 'data-pjax-target' attribute.
*/
$('.cms .cms-panel-link').entwine({
onclick: function(e) {
if($(this).hasClass('external-link')) {
e.stopPropagation();
return;
}
var href = this.attr('href'),
url = (href && !href.match(/^#/)) ? href : this.data('href'),
data = {pjax: this.data('pjaxTarget')};
$('.cms-container').loadPanel(url, null, data);
e.preventDefault();
}
});
/**
* Does an ajax loads of the link's 'href' attribute via ajax and displays any FormResponse messages from the CMS.
* Little helper to avoid repetition, and make it easy to trigger actions via a link,
* without reloading the page, changing the URL, or loading in any new panel content.
*/
$('.cms .ss-ui-button-ajax').entwine({
onclick: function(e) {
$(this).removeClass('ui-button-text-only');
$(this).addClass('ss-ui-button-loading ui-button-text-icons');
var loading = $(this).find(".ss-ui-loading-icon");
if(loading.length < 1) {
loading = $("<span></span>").addClass('ss-ui-loading-icon ui-button-icon-primary ui-icon');
$(this).prepend(loading);
}
loading.show();
var href = this.attr('href'), url = href ? href : this.data('href');
jQuery.ajax({
url: url,
// Ensure that form view is loaded (rather than whole "Content" template)
complete: function(xmlhttp, status) {
var msg = (xmlhttp.getResponseHeader('X-Status')) ? xmlhttp.getResponseHeader('X-Status') : xmlhttp.responseText;
try {
if (typeof msg != "undefined" && msg !== null) eval(msg);
}
catch(e) {}
loading.hide();
$(".cms-container").refresh();
$(this).removeClass('ss-ui-button-loading ui-button-text-icons');
$(this).addClass('ui-button-text-only');
},
dataType: 'html'
});
e.preventDefault();
}
});
/**
* Trigger dialogs with iframe based on the links href attribute (see ssui-core.js).
*/
$('.cms .ss-ui-dialog-link').entwine({
UUID: null,
onmatch: function() {
this._super();
this.setUUID(new Date().getTime());
},
onunmatch: function() {
this._super();
},
onclick: function() {
this._super();
var self = this, id = 'ss-ui-dialog-' + this.getUUID();
var dialog = $('#' + id);
if(!dialog.length) {
dialog = $('<div class="ss-ui-dialog" id="' + id + '" />');
$('body').append(dialog);
}
var extraClass = this.data('popupclass')?this.data('popupclass'):'';
dialog.ssdialog({iframeUrl: this.attr('href'), autoOpen: true, dialogExtraClass: extraClass});
return false;
}
});
/**
* Add styling to all contained buttons, and create buttonsets if required.
*/
$('.cms-content .Actions').entwine({
onmatch: function() {
this.find('.ss-ui-button').click(function() {
var form = this.form;
// forms don't natively store the button they've been triggered with
if(form) {
form.clickedButton = this;
// Reset the clicked button shortly after the onsubmit handlers
// have fired on the form
setTimeout(function() {
form.clickedButton = null;
}, 10);
}
});
this.redraw();
this._super();
},
onunmatch: function() {
this._super();
},
redraw: function() {
if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
// Remove whitespace to avoid gaps with inline elements
this.contents().filter(function() {
return (this.nodeType == 3 && !/\S/.test(this.nodeValue));
}).remove();
// Init buttons if required
this.find('.ss-ui-button').each(function() {
if(!$(this).data('button')) $(this).button();
});
// Mark up buttonsets
this.find('.ss-ui-buttonset').buttonset();
}
});
/**
* Duplicates functionality in DateField.js, but due to using entwine we can match
* the DOM element on creation, rather than onclick - which allows us to decorate
* the field with a calendar icon
*/
$('.cms .field.date input.text').entwine({
onmatch: function() {
var holder = $(this).parents('.field.date:first'), config = holder.data();
if(!config.showcalendar) {
this._super();
return;
}
config.showOn = 'button';
if(config.locale && $.datepicker.regional[config.locale]) {
config = $.extend(config, $.datepicker.regional[config.locale], {});
}
$(this).datepicker(config);
// // Unfortunately jQuery UI only allows configuration of icon images, not sprites
// this.next('button').button('option', 'icons', {primary : 'ui-icon-calendar'});
this._super();
},
onunmatch: function() {
this._super();
}
});
/**
* Styled dropdown select fields via chosen. Allows things like search and optgroup
* selection support. Rather than manually adding classes to selects we want
* styled, we style everything but the ones we tell it not to.
*
* For the CMS we also need to tell the parent div that it has a select so
* we can fix the height cropping.
*/
$('.cms .field.dropdown select, .cms .field select[multiple], .fieldholder-small select.dropdown').entwine({
onmatch: function() {
if(this.is('.no-chzn')) {
this._super();
return;
}
// Explicitly disable default placeholder if no custom one is defined
if(!this.data('placeholder')) this.data('placeholder', ' ');
// We could've gotten stale classes and DOM elements from deferred cache.
this.removeClass('has-chzn chzn-done');
this.siblings('.chzn-container').remove();
// Apply Chosen
applyChosen(this);
this._super();
},
onunmatch: function() {
this._super();
}
});
$(".cms-panel-layout").entwine({
redraw: function() {
if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
}
});
/**
* Overload the default GridField behaviour (open a new URL in the browser)
* with the CMS-specific ajax loading.
*/
$('.cms .ss-gridfield').entwine({
showDetailView: function(url) {
// Include any GET parameters from the current URL, as the view state might depend on it.
// For example, a list prefiltered through external search criteria might be passed to GridField.
var params = window.location.search.replace(/^\?/, '');
if(params) url = $.path.addSearchParams(url, params);
$('.cms-container').loadPanel(url);
}
});
/**
* Generic search form in the CMS, often hooked up to a GridField results display.
*/
$('.cms-search-form').entwine({
onsubmit: function(e) {
// Remove empty elements and make the URL prettier
var nonEmptyInputs,
url;
nonEmptyInputs = this.find(':input:not(:submit)').filter(function() {
// Use fieldValue() from jQuery.form plugin rather than jQuery.val(),
// as it handles checkbox values more consistently
var vals = $.grep($(this).fieldValue(), function(val) { return (val);});
return (vals.length);
});
url = this.attr('action');
if(nonEmptyInputs.length) {
url = $.path.addSearchParams(url, nonEmptyInputs.serialize());
}
var container = this.closest('.cms-container');
container.find('.cms-edit-form').tabs('select',0); //always switch to the first tab (list view) when searching
container.loadPanel(url, "", {}, true);
return false;
}
});
/**
* Reset button handler. IE8 does not bubble reset events to
*/
$(".cms-search-form button[type=reset], .cms-search-form input[type=reset]").entwine({
onclick: function(e) {
e.preventDefault();
var form = $(this).parents('form');
form.clearForm();
form.find(".dropdown select").prop('selectedIndex', 0).trigger("liszt:updated"); // Reset chosen.js
form.submit();
}
});
/**
* Allows to lazy load a panel, by leaving it empty
* and declaring a URL to load its content via a 'url' HTML5 data attribute.
* The loaded HTML is cached, with cache key being the 'url' attribute.
* In order for this to work consistently, we assume that the responses are stateless.
* To avoid caching, add a 'deferred-no-cache' to the node.
*/
window._panelDeferredCache = {};
$('.cms-panel-deferred').entwine({
onadd: function() {
this._super();
this.redraw();
},
onremove: function() {
if(window.debug) console.log('saving', this.data('url'), this);
// Save the HTML state at the last possible moment.
// Don't store the DOM to avoid memory leaks.
if(!this.data('deferredNoCache')) window._panelDeferredCache[this.data('url')] = this.html();
this._super();
},
redraw: function() {
if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
var self = this, url = this.data('url');
if(!url) throw 'Elements of class .cms-panel-deferred need a "data-url" attribute';
this._super();
// If the node is empty, try to either load it from cache or via ajax.
if(!this.children().length) {
if(!this.data('deferredNoCache') && typeof window._panelDeferredCache[url] !== 'undefined') {
this.html(window._panelDeferredCache[url]);
} else {
this.addClass('loading');
$.ajax({
url: url,
complete: function() {
self.removeClass('loading');
},
success: function(data, status, xhr) {
self.html(data);
}
});
}
}
}
});
/**
* Lightweight wrapper around jQuery UI tabs.
* Ensures that anchor links are set properly,
* and any nested tabs are scrolled if they have
* their height explicitly set. This is important
* for forms inside the CMS layout.
*/
$('.cms-tabset').entwine({
onadd: function() {
// Can't name redraw() as it clashes with other CMS entwine classes
this.redrawTabs();
this._super();
},
onremove: function() {
if (this.data('tabs')) this.tabs('destroy');
this._super();
},
redrawTabs: function() {
this.rewriteHashlinks();
var id = this.attr('id'), activeTab = this.find('ul:first .ui-tabs-active');
if(!this.data('uiTabs')) this.tabs({
active: (activeTab.index() != -1) ? activeTab.index() : 0,
beforeLoad: function(e, ui) {
// Disable automatic ajax loading of tabs without matching DOM elements,
// determining if the current URL differs from the tab URL is too error prone.
return false;
},
activate: function(e, ui) {
// Accessibility: Simulate click to trigger panel load when tab is focused
// by a keyboard navigation event rather than a click
if(ui.newTab) {
ui.newTab.find('.cms-panel-link').click();
}
// Usability: Hide actions for "readonly" tabs (which don't contain any editable fields)
var actions = $(this).closest('form').find('.Actions');
if($(ui.newTab).closest('li').hasClass('readonly')) {
actions.fadeOut();
} else {
actions.show();
}
}
});
},
/**
* Ensure hash links are prefixed with the current page URL,
* otherwise jQuery interprets them as being external.
*/
rewriteHashlinks: function() {
$(this).find('ul a').each(function() {
if (!$(this).attr('href')) return;
var matches = $(this).attr('href').match(/#.*/);
if(!matches) return;
$(this).attr('href', document.location.href.replace(/#.*/, '') + matches[0]);
});
}
});
/**
* CMS content filters
*/
$('#filters-button').entwine({
onmatch: function () {
this._super();
this.data('collapsed', true); // The current collapsed state of the element.
this.data('animating', false); // True if the element is currently animating.
},
onunmatch: function () {
this._super();
},
showHide: function () {
var self = this,
$filters = $('.cms-content-filters').first(),
collapsed = this.data('collapsed');
// Prevent the user from spamming the UI with animation requests.
if (this.data('animating')) {
return;
}
this.toggleClass('active');
this.data('animating', true);
// Slide the element down / up based on it's current collapsed state.
$filters[collapsed ? 'slideDown' : 'slideUp']({
complete: function () {
// Update the element's state.
self.data('collapsed', !collapsed);
self.data('animating', false);
}
});
},
onclick: function () {
this.showHide();
}
});
});
}(jQuery));
var statusMessage = function(text, type) {
text = jQuery('<div/>').text(text).html(); // Escape HTML entities in text
jQuery.noticeAdd({text: text, type: type, stayTime: 5000, inEffect: {left: '0', opacity: 'show'}});
};
var errorMessage = function(text) {
jQuery.noticeAdd({text: text, type: 'error', stayTime: 5000, inEffect: {left: '0', opacity: 'show'}});
};

View File

@ -1,20 +0,0 @@
(function($) {
$.entwine('ss', function($){
$('.memberdatetimeoptionset').entwine({
onmatch: function() {
this.find('.description .toggle-content').hide();
this._super();
}
});
$('.memberdatetimeoptionset .toggle').entwine({
onclick: function(e) {
jQuery(this).closest('.description').find('.toggle-content').toggle();
return false;
}
});
});
}(jQuery));

View File

@ -1,35 +0,0 @@
/**
* File: MemberImportForm.js
*/
(function($) {
$.entwine('ss', function($){
/**
* Class: .import-form .advanced
*/
$('.import-form .advanced').entwine({
onmatch: function() {
this._super();
this.hide();
},
onunmatch: function() {
this._super();
}
});
/**
* Class: .import-form a.toggle-advanced
*/
$('.import-form a.toggle-advanced').entwine({
/**
* Function: onclick
*/
onclick: function(e) {
this.parents('form:eq(0)').find('.advanced').toggle();
return false;
}
});
});
}(jQuery));

View File

@ -1,35 +0,0 @@
/**
* File: ModelAdmin.js
*/
(function($) {
$.entwine('ss', function($){
$('.cms-content-tools #Form_SearchForm').entwine({
onsubmit: function(e) {
//We need to trigger handleStateChange() explicitly, otherwise handleStageChange()
//doesn't called if landing from another section of cms
this.trigger('beforeSubmit');
}
});
/**
* Class: .importSpec
*
* Toggle import specifications
*/
$('.importSpec').entwine({
onmatch: function() {
this.find('div.details').hide();
this.find('a.detailsLink').click(function() {
$('#' + $(this).attr('href').replace(/.*#/,'')).slideToggle();
return false;
});
this._super();
},
onunmatch: function() {
this._super();
}
});
});
})(jQuery);

View File

@ -1,79 +0,0 @@
/**
* File: SecurityAdmin.js
*/
(function($) {
var refreshAfterImport = function(e) {
// Check for a message <div>, an indication that the form has been submitted.
var existingFormMessage = $($(this).contents()).find('.message');
if(existingFormMessage && existingFormMessage.html()) {
// Refresh member listing
var memberTableField = $(window.parent.document).find('#Form_EditForm_Members').get(0);
if(memberTableField) memberTableField.refresh();
// Refresh tree
var tree = $(window.parent.document).find('.cms-tree').get(0);
if(tree) tree.reload();
}
};
/**
* Refresh the member listing every time the import iframe is loaded,
* which is most likely a form submission.
*/
$('#MemberImportFormIframe, #GroupImportFormIframe').entwine({
onadd: function() {
this._super();
// TODO entwine can't seem to bind to iframe load events
$(this).bind('load', refreshAfterImport);
}
});
$.entwine('ss', function($){
/**
* Class: #Permissions .checkbox[value=ADMIN]
*
* Automatically check and disable all checkboxes if ADMIN permissions are selected.
* As they're disabled, any changes won't be submitted (which is intended behaviour),
* checking all boxes is purely presentational.
*/
$('.permissioncheckboxset .checkbox[value=ADMIN]').entwine({
onmatch: function() {
this.toggleCheckboxes();
this._super();
},
onunmatch: function() {
this._super();
},
/**
* Function: onclick
*/
onclick: function(e) {
this.toggleCheckboxes();
},
/**
* Function: toggleCheckboxes
*/
toggleCheckboxes: function() {
var self = this,
checkboxes = this.parents('.field:eq(0)').find('.checkbox').not(this);
if(this.is(':checked')) {
checkboxes.each(function() {
$(this).data('SecurityAdmin.oldChecked', $(this).is(':checked'));
$(this).data('SecurityAdmin.oldDisabled', $(this).is(':disabled'));
$(this).prop('disabled', true);
$(this).prop('checked', true);
});
} else {
checkboxes.each(function() {
$(this).prop('checked', $(this).data('SecurityAdmin.oldChecked'));
$(this).prop('disabled', $(this).data('SecurityAdmin.oldDisabled'));
});
}
}
});
});
}(jQuery));

View File

@ -1,57 +0,0 @@
/**
* Fits an element's height to its parent by substracting
* all (visible) siblings heights from the element.
* Caution: This will set overflow: hidden on the parent
*
* Copyright 2009 Ingo Schommer, SilverStripe Ltd.
* Licensed under MIT License: http://www.opensource.org/licenses/mit-license.php
*
* @todo Implement selectors to ignore certain elements
*
* @author Ingo Schommer, SilverStripe Ltd.
* @version 0.1
*/
jQuery.fn.extend({
fitHeightToParent: function() {
return jQuery(this).each(function() {
var $this = jQuery(this);
var boxmodel = ['marginTop','marginBottom','paddingTop','paddingBottom','borderBottomWidth','borderTopWidth'];
// don't bother if element or parent arent visible,
// we won't get height readings
if($this.is(':visible') && $this.parent().is(':visible')) {
// we set overflow = hidden so that large children don't muck things up in IE6 box model
var origParentOverflow = $this.parent().css('overflow');
$this.parent().css('overflow', 'hidden');
// get height from parent without any margins as a starting point,
// and reduce any top/bottom paddings
var height = $this.parent().innerHeight()
- parseFloat($this.parent().css('paddingTop'))
- parseFloat($this.parent().css('paddingBottom'));
// substract height of any siblings of the current element
// including their margins/paddings/borders
$this.siblings(':visible').filter(function() {
// remove all absolutely positioned elements
return (jQuery(this).css('position') != 'absolute');
}).each(function() {
height -= jQuery(this).outerHeight(true);
});
// remove margins/paddings/borders on inner element
jQuery.each(boxmodel, function(i, name) {
height -= parseFloat($this.css(name)) || 0;
});
// set new height
$this.height(height);
// Reset overflow
$this.parent().css('overflow', origParentOverflow);
}
});
}
});

View File

@ -1,11 +0,0 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script type="text/javascript" src="jquery.fitheighttoparent.js"></script>
</head>
<body>
test
</body>
</html>

View File

@ -1,150 +0,0 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script type='text/javascript' src='http://getfirebug.com/releases/lite/1.2/firebug-lite-compressed.js'></script>
<script type="text/javascript" src="../jquery.fitheighttoparent.js"></script>
<link rel="stylesheet" href="http://dev.jquery.com/view/trunk/qunit/testsuite.css" type="text/css" media="screen" />
<style type="text/css">
.outer {background: #333;}
.inner {background: #f0f;}
.sibling {background: #bbb;}
#test1-parent {height: 200px; margin: 10px 0; padding: 15px 10px; border: 5px solid #000;}
#test1-sibling-before {height: 20px; margin: 3px 0; padding: 2px 0;}
#test1-sibling-after {height: 30px; margin: 3px 0; padding: 2px 0;}
#test2-parent {height: 200px; margin: 10px 0; padding: 15px 10px; }
#test2-sibling-absolute {height: 20px; position: absolute;}
#test3-parent {height: 200px; margin: 10px 0; padding: 15px 10px; }
#test3-sibling {height: 20px;}
#test3-sibling-hidden {height: 30px; display: none;}
#test4-parent {height: 200px; margin: 10px 0; padding: 15px 10px; }
#test4-sibling {height: 20px;}
#test4 {margin: 15px 0; padding: 10px 0;border: 5px solid #000;}
#test5-parent {height: 200px; margin: 10px 0; padding: 15px 10px; }
#test5-sibling {height: 20px;}
#test5 {margin: 15px 0; padding: 10px 0;border: 5px solid #000;}
#test6-parent {height: 200px; margin: 10px 0; padding: 15px 10px; position: relative; width: 100%;}
#test6-sibling {height: 20px;}
#test6 {overflow: auto; }
</style>
<script>
$(document).ready(function(){
test("with inline siblings, margins/paddings/borders on parent", function() {
equals(
jQuery('#test1').fitHeightToParent().height(),
130
);
});
test("with absolute siblings", function() {
equals(
jQuery('#test2').fitHeightToParent().height(),
200
);
});
test("with hidden siblings", function() {
equals(
jQuery('#test3').fitHeightToParent().height(),
180
);
});
test("with margins/paddings/borders on resized element", function() {
equals(
jQuery('#test4').fitHeightToParent().height(),
120
);
});
test("form with fieldset", function() {
equals(
jQuery('#test5').fitHeightToParent().height(),
120
);
});
test("overflow auto with long inner element", function() {
equals(
jQuery('#test6').fitHeightToParent().height(),
180
);
});
});
</script>
</head>
<body>
<script type="text/javascript" src="http://jqueryjs.googlecode.com/svn/trunk/qunit/testrunner.js"></script>
<h1>jquery.fitheighttoparent unit test</h1>
<h2 id="banner"></h2>
<h2 id="userAgent"></h2>
<ol id="tests"></ol>
<div id="main"></div>
<div id="test1-parent" class="outer">
<div id="test1-sibling-before" class="sibling"></div>
<div id="test1" class="inner">
<p>test 1</p>
</div>
<div id="test1-sibling-after" class="sibling"></div>
</div>
<div id="test2-parent" class="outer">
<div id="test2-sibling-absolute" class="sibling"></div>
<div id="test2" class="inner">
<p>test 2</p>
</div>
</div>
<div id="test3-parent" class="outer">
<div id="test3-sibling" class="sibling"></div>
<div id="test3" class="inner">
<p>test 3</p>
</div>
<div id="test3-sibling-hidden" class="sibling"></div>
</div>
<div id="test4-parent" class="outer">
<div id="test4-sibling" class="sibling"></div>
<div id="test4" class="inner">
<p>test 4</p>
</div>
</div>
<form action="#" method="POST" id="test5-parent" class="outer">
<fieldset id="test5" class="inner">
<p>test 5</p>
<input type="text">
</fieldset>
<div id="test5-sibling" class="sibling"></div>
</form>
<div id="test6-parent" class="outer">
<div id="test6-sibling" class="sibling"></div>
<div id="test6" class="inner">
<p>test 6</p>
<p>Suspendisse vestibulum dignissim quam. Integer vel augue. Phasellus nulla purus, interdum ac, venenatis non, varius rutrum, leo. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Duis a eros. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Fusce magna mi, porttitor quis, convallis eget, sodales ac, urna. Phasellus luctus venenatis magna. Vivamus eget lacus. Nunc tincidunt convallis tortor. Duis eros mi, dictum vel, fringilla sit amet, fermentum id, sem. Phasellus nunc enim, faucibus ut, laoreet in, consequat id, metus. Vivamus dignissim. Cras lobortis tempor velit. Phasellus nec diam ac nisl lacinia tristique. Nullam nec metus id mi dictum dignissim. Nullam quis wisi non sem lobortis condimentum. Phasellus pulvinar, nulla non aliquam eleifend, tortor wisi scelerisque felis, in sollicitudin arcu ante lacinia leo.</p>
<p>
Suspendisse vestibulum dignissim quam. Integer vel augue. Phasellus nulla purus, interdum ac, venenatis non, varius rutrum, leo. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Duis a eros. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Fusce magna mi, porttitor quis, convallis eget, sodales ac, urna. Phasellus luctus venenatis magna. Vivamus eget lacus. Nunc tincidunt convallis tortor. Duis eros mi, dictum vel, fringilla sit amet, fermentum id, sem. Phasellus nunc enim, faucibus ut, laoreet in, consequat id, metus. Vivamus dignissim. Cras lobortis tempor velit. Phasellus nec diam ac nisl lacinia tristique. Nullam nec metus id mi dictum dignissim. Nullam quis wisi non sem lobortis condimentum. Phasellus pulvinar, nulla non aliquam eleifend, tortor wisi scelerisque felis, in sollicitudin arcu ante lacinia leo.</p>
<p>
Suspendisse vestibulum dignissim quam. Integer vel augue. Phasellus nulla purus, interdum ac, venenatis non, varius rutrum, leo. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Duis a eros. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Fusce magna mi, porttitor quis, convallis eget, sodales ac, urna. Phasellus luctus venenatis magna. Vivamus eget lacus. Nunc tincidunt convallis tortor. Duis eros mi, dictum vel, fringilla sit amet, fermentum id, sem. Phasellus nunc enim, faucibus ut, laoreet in, consequat id, metus. Vivamus dignissim. Cras lobortis tempor velit. Phasellus nec diam ac nisl lacinia tristique. Nullam nec metus id mi dictum dignissim. Nullam quis wisi non sem lobortis condimentum. Phasellus pulvinar, nulla non aliquam eleifend, tortor wisi scelerisque felis, in sollicitudin arcu ante lacinia leo.</p>
</div>
</div>
</body>
</html>

View File

@ -1,49 +0,0 @@
(function($){
var getHTML = function(el) {
var clone = el.cloneNode(true);
var div = $('<div></div>');
div.append(clone);
return div.html();
}
$.leaktools = {
logDuplicateElements: function(){
var els = $('*');
var dirty = false;
els.each(function(i, a){
els.not(a).each(function(j, b){
if (getHTML(a) == getHTML(b)) {
dirty = true;
console.log(a, b);
}
})
})
if (!dirty) console.log('No duplicates found');
},
logUncleanedElements: function(clean){
$.each($.cache, function(){
var source = this.handle && this.handle.elem;
if (!source) return;
var parent = source;
while (parent && parent.nodeType == 1) parent = parent.parentNode;
if (!parent) {
console.log('Unattached', source);
console.log(this.events);
if (clean) $(source).unbind().remove();
}
else if (parent !== document) console.log('Attached, but to', parent, 'not our document', source);
})
}
};
})(jQuery);

View File

@ -1,256 +0,0 @@
(function($) {
// Copyright (c) 2011 John Resig, http://jquery.com/
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//define vars for interal use
var $window = $( window ),
$html = $( 'html' ),
$head = $( 'head' ),
//url path helpers for use in relative url management
path = {
// This scary looking regular expression parses an absolute URL or its relative
// variants (protocol, site, document, query, and hash), into the various
// components (protocol, host, path, query, fragment, etc that make up the
// URL as well as some other commonly used sub-parts. When used with RegExp.exec()
// or String.match, it parses the URL into a results array that looks like this:
//
// [0]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread#msg-content
// [1]: http://jblas:password@mycompany.com:8080/mail/inbox?msg=1234&type=unread
// [2]: http://jblas:password@mycompany.com:8080/mail/inbox
// [3]: http://jblas:password@mycompany.com:8080
// [4]: http:
// [5]: //
// [6]: jblas:password@mycompany.com:8080
// [7]: jblas:password
// [8]: jblas
// [9]: password
// [10]: mycompany.com:8080
// [11]: mycompany.com
// [12]: 8080
// [13]: /mail/inbox
// [14]: /mail/
// [15]: inbox
// [16]: ?msg=1234&type=unread
// [17]: #msg-content
//
urlParseRE: /^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/,
//Parse a URL into a structure that allows easy access to
//all of the URL components by name.
parseUrl: function( url ) {
// If we're passed an object, we'll assume that it is
// a parsed url object and just return it back to the caller.
if ( $.type( url ) === "object" ) {
return url;
}
var matches = path.urlParseRE.exec( url || "" ) || [];
// Create an object that allows the caller to access the sub-matches
// by name. Note that IE returns an empty string instead of undefined,
// like all other browsers do, so we normalize everything so its consistent
// no matter what browser we're running on.
return {
href: matches[ 0 ] || "",
hrefNoHash: matches[ 1 ] || "",
hrefNoSearch: matches[ 2 ] || "",
domain: matches[ 3 ] || "",
protocol: matches[ 4 ] || "",
doubleSlash: matches[ 5 ] || "",
authority: matches[ 6 ] || "",
username: matches[ 8 ] || "",
password: matches[ 9 ] || "",
host: matches[ 10 ] || "",
hostname: matches[ 11 ] || "",
port: matches[ 12 ] || "",
pathname: matches[ 13 ] || "",
directory: matches[ 14 ] || "",
filename: matches[ 15 ] || "",
search: matches[ 16 ] || "",
hash: matches[ 17 ] || ""
};
},
//Turn relPath into an asbolute path. absPath is
//an optional absolute path which describes what
//relPath is relative to.
makePathAbsolute: function( relPath, absPath ) {
if ( relPath && relPath.charAt( 0 ) === "/" ) {
return relPath;
}
relPath = relPath || "";
absPath = absPath ? absPath.replace( /^\/|(\/[^\/]*|[^\/]+)$/g, "" ) : "";
var absStack = absPath ? absPath.split( "/" ) : [],
relStack = relPath.split( "/" );
for ( var i = 0; i < relStack.length; i++ ) {
var d = relStack[ i ];
switch ( d ) {
case ".":
break;
case "..":
if ( absStack.length ) {
absStack.pop();
}
break;
default:
absStack.push( d );
break;
}
}
return "/" + absStack.join( "/" );
},
//Returns true if both urls have the same domain.
isSameDomain: function( absUrl1, absUrl2 ) {
return path.parseUrl( absUrl1 ).domain === path.parseUrl( absUrl2 ).domain;
},
//Returns true for any relative variant.
isRelativeUrl: function( url ) {
// All relative Url variants have one thing in common, no protocol.
return path.parseUrl( url ).protocol === "";
},
//Returns true for an absolute url.
isAbsoluteUrl: function( url ) {
return path.parseUrl( url ).protocol !== "";
},
//Turn the specified realtive URL into an absolute one. This function
//can handle all relative variants (protocol, site, document, query, fragment).
makeUrlAbsolute: function( relUrl, absUrl ) {
if ( !path.isRelativeUrl( relUrl ) ) {
return relUrl;
}
var relObj = path.parseUrl( relUrl ),
absObj = path.parseUrl( absUrl ),
protocol = relObj.protocol || absObj.protocol,
doubleSlash = relObj.protocol ? relObj.doubleSlash : ( relObj.doubleSlash || absObj.doubleSlash ),
authority = relObj.authority || absObj.authority,
hasPath = relObj.pathname !== "",
pathname = path.makePathAbsolute( relObj.pathname || absObj.filename, absObj.pathname ),
search = relObj.search || ( !hasPath && absObj.search ) || "",
hash = relObj.hash;
return protocol + doubleSlash + authority + pathname + search + hash;
},
//Add search (aka query) params to the specified url.
// 2013-12-06 ischommer: Customized to merge with existing keys
addSearchParams: function( url, params ) {
var u = path.parseUrl( url ),
params = ( typeof params === "string" ) ? path.convertSearchToArray( params ) : params,
newParams = $.extend( path.convertSearchToArray( u.search ), params );
return u.hrefNoSearch + '?' + $.param( newParams ) + ( u.hash || "" );
},
// 2013-12-06 ischommer: Added to allow merge with existing keys
getSearchParams: function(url) {
var u = path.parseUrl( url );
return path.convertSearchToArray( u.search );
},
// Converts query strings (foo=bar&baz=bla) to a hash.
// TODO Handle repeating elements (e.g. arr[]=one&arr[]=two)
// 2013-12-06 ischommer: Added to allow merge with existing keys
convertSearchToArray: function(search) {
var params = {},
search = search.replace( /^\?/, '' ),
parts = search ? search.split( '&' ) : [], i, tmp;
for(i=0; i < parts.length; i++) {
tmp = parts[i].split( '=' );
params[tmp[0]] = tmp[1];
}
return params;
},
convertUrlToDataUrl: function( absUrl ) {
var u = path.parseUrl( absUrl );
if ( path.isEmbeddedPage( u ) ) {
// For embedded pages, remove the dialog hash key as in getFilePath(),
// otherwise the Data Url won't match the id of the embedded Page.
return u.hash.split( dialogHashKey )[0].replace( /^#/, "" );
} else if ( path.isSameDomain( u, document ) ) {
return u.hrefNoHash.replace( document.domain, "" );
}
return absUrl;
},
//get path from current hash, or from a file path
get: function( newPath ) {
if( newPath === undefined ) {
newPath = location.hash;
}
return path.stripHash( newPath ).replace( /[^\/]*\.[^\/*]+$/, '' );
},
//return the substring of a filepath before the sub-page key, for making a server request
getFilePath: function( path ) {
var splitkey = '&' + $.mobile.subPageUrlKey;
return path && path.split( splitkey )[0].split( dialogHashKey )[0];
},
//set location hash to path
set: function( path ) {
location.hash = path;
},
//test if a given url (string) is a path
//NOTE might be exceptionally naive
isPath: function( url ) {
return ( /\// ).test( url );
},
//return a url path with the window's location protocol/hostname/pathname removed
clean: function( url ) {
return url.replace( document.domain, "" );
},
//just return the url without an initial #
stripHash: function( url ) {
return url.replace( /^#/, "" );
},
//remove the preceding hash, any query params, and dialog notations
cleanHash: function( hash ) {
return path.stripHash( hash.replace( /\?.*$/, "" ).replace( dialogHashKey, "" ) );
},
//check whether a url is referencing the same domain, or an external domain or different protocol
//could be mailto, etc
isExternal: function( url ) {
var u = path.parseUrl( url );
return u.protocol && u.domain !== document.domain ? true : false;
},
hasProtocol: function( url ) {
return ( /^(:?\w+:)/ ).test( url );
}
};
$.path = path;
}(jQuery));

View File

@ -1,325 +0,0 @@
(function($) {
/**
* Allows icon definition via HTML5 data attrs for easier handling in PHP.
*
* Adds an alternative appearance so we can toggle back and forth between them
* and register event handlers to add custom styling and behaviour. Example use
* is in the CMS with the saving buttons - depending on the page's state one of
* them will either say "Save draft" or "Saved", and will have different colour.
*/
$.widget('ssui.button', $.ui.button, {
options: {
alternate: {
icon: null,
text: null
},
showingAlternate: false
},
/**
* Switch between the alternate appearances.
*/
toggleAlternate: function() {
if (this._trigger('ontogglealternate')===false) return;
// Only switch to alternate if it has been enabled through options.
if (!this.options.alternate.icon && !this.options.alternate.text) return;
this.options.showingAlternate = !this.options.showingAlternate;
this.refresh();
},
/**
* Adjust the appearance to fit with the current settings.
*/
_refreshAlternate: function() {
this._trigger('beforerefreshalternate');
// Only switch to alternate if it has been enabled through options.
if (!this.options.alternate.icon && !this.options.alternate.text) return;
if (this.options.showingAlternate) {
this.element.find('.ui-button-icon-primary').hide();
this.element.find('.ui-button-text').hide();
this.element.find('.ui-button-icon-alternate').show();
this.element.find('.ui-button-text-alternate').show();
}
else {
this.element.find('.ui-button-icon-primary').show();
this.element.find('.ui-button-text').show();
this.element.find('.ui-button-icon-alternate').hide();
this.element.find('.ui-button-text-alternate').hide();
}
this._trigger('afterrefreshalternate');
},
/**
* Construct button - pulls in options from data attributes.
* Injects new elements for alternate appearance (if requested via options).
*/
_resetButton: function() {
var iconPrimary = this.element.data('icon-primary'),
iconSecondary = this.element.data('icon-secondary');
if (!iconPrimary) iconPrimary = this.element.data('icon');
// TODO Move prefix out of this method, without requriing it for every icon definition in a data attr
if(iconPrimary) this.options.icons.primary = 'btn-icon-' + iconPrimary;
if(iconSecondary) this.options.icons.secondary = 'btn-icon-' + iconSecondary;
$.ui.button.prototype._resetButton.call(this);
// Pull options from data attributes. Overriden by explicit options given on widget creation.
if (!this.options.alternate.text) {
this.options.alternate.text = this.element.data('text-alternate');
}
if (!this.options.alternate.icon) {
this.options.alternate.icon = this.element.data('icon-alternate');
}
if (!this.options.showingAlternate) {
this.options.showingAlternate = this.element.hasClass('ss-ui-alternate');
}
// Create missing elements.
if (this.options.alternate.icon) {
this.buttonElement.append(
"<span class='ui-button-icon-alternate ui-button-icon-primary ui-icon btn-icon-"
+ this.options.alternate.icon + "'></span>"
);
}
if (this.options.alternate.text) {
this.buttonElement.append(
"<span class='ui-button-text-alternate ui-button-text'>" + this.options.alternate.text + "</span>"
);
}
this._refreshAlternate();
},
refresh: function() {
$.ui.button.prototype.refresh.call(this);
this._refreshAlternate();
},
destroy: function() {
this.element.find('.ui-button-text-alternate').remove();
this.element.find('.ui-button-icon-alternate').remove();
$.ui.button.prototype.destroy.call( this );
}
});
/**
* Extends jQueryUI dialog with iframe abilities (and related resizing logic),
* and sets some CMS-wide defaults.
*
* Additional settings:
* - 'autoPosition': Automatically reposition window on resize based on 'position' option
* - 'widthRatio': Sets width based on percentage of window (value between 0 and 1)
* - 'heightRatio': Sets width based on percentage of window (value between 0 and 1)
* - 'reloadOnOpen': Reloads the iframe whenever the dialog is reopened
* - 'iframeUrl': Create an iframe element and load this URL when the dialog is created
*/
$.widget("ssui.ssdialog", $.ui.dialog, {
options: {
// Custom properties
iframeUrl: '',
reloadOnOpen: true,
dialogExtraClass: '',
// Defaults
modal: true,
bgiframe: true,
autoOpen: false,
autoPosition: true,
minWidth: 500,
maxWidth: 800,
minHeight: 300,
maxHeight: 700,
widthRatio: 0.8,
heightRatio: 0.8,
resizable: false
},
_create: function() {
$.ui.dialog.prototype._create.call(this);
var self = this;
// Create iframe
var iframe = $('<iframe marginWidth="0" marginHeight="0" frameBorder="0" scrolling="auto"></iframe>');
iframe.bind('load', function(e) {
if($(this).attr('src') == 'about:blank') return;
iframe.addClass('loaded').show(); // more reliable than 'src' attr check (in IE)
self._resizeIframe();
self.uiDialog.removeClass('loading');
}).hide();
if(this.options.dialogExtraClass) this.uiDialog.addClass(this.options.dialogExtraClass);
this.element.append(iframe);
// Let the iframe handle its scrolling
if(this.options.iframeUrl) this.element.css('overflow', 'hidden');
},
open: function() {
$.ui.dialog.prototype.open.call(this);
var self = this, iframe = this.element.children('iframe');
// Load iframe
if(this.options.iframeUrl && (!iframe.hasClass('loaded') || this.options.reloadOnOpen)) {
iframe.hide();
iframe.attr('src', this.options.iframeUrl);
this.uiDialog.addClass('loading');
}
// Resize events
$(window).bind('resize.ssdialog', function() {self._resizeIframe();});
},
close: function() {
$.ui.dialog.prototype.close.call(this);
this.uiDialog.unbind('resize.ssdialog');
$(window).unbind('resize.ssdialog');
},
_resizeIframe: function() {
var opts = {}, newWidth, newHeight, iframe = this.element.children('iframe');;
if(this.options.widthRatio) {
newWidth = $(window).width() * this.options.widthRatio;
if(this.options.minWidth && newWidth < this.options.minWidth) {
opts.width = this.options.minWidth
} else if(this.options.maxWidth && newWidth > this.options.maxWidth) {
opts.width = this.options.maxWidth;
} else {
opts.width = newWidth;
}
}
if(this.options.heightRatio) {
newHeight = $(window).height() * this.options.heightRatio;
if(this.options.minHeight && newHeight < this.options.minHeight) {
opts.height = this.options.minHeight
} else if(this.options.maxHeight && newHeight > this.options.maxHeight) {
opts.height = this.options.maxHeight;
} else {
opts.height = newHeight;
}
}
if(!jQuery.isEmptyObject(opts)) {
this._setOptions(opts);
// Resize iframe within dialog
iframe.attr('width',
opts.width
- parseFloat(this.element.css('paddingLeft'))
- parseFloat(this.element.css('paddingRight'))
);
iframe.attr('height',
opts.height
- parseFloat(this.element.css('paddingTop'))
- parseFloat(this.element.css('paddingBottom'))
);
// Enforce new position
if(this.options.autoPosition) {
this._setOption("position", this.options.position);
}
}
}
});
$.widget("ssui.titlebar", {
_create: function() {
this.originalTitle = this.element.attr('title');
var self = this;
var options = this.options;
var title = options.title || this.originalTitle || '&nbsp;';
var titleId = $.ui.dialog.getTitleId(this.element);
this.element.parent().addClass('ui-dialog');
var uiDialogTitlebar = this.element.
addClass(
'ui-dialog-titlebar ' +
'ui-widget-header ' +
'ui-corner-all ' +
'ui-helper-clearfix'
);
// By default, the
if(options.closeButton) {
var uiDialogTitlebarClose = $('<a href="#"/>')
.addClass(
'ui-dialog-titlebar-close ' +
'ui-corner-all'
)
.attr('role', 'button')
.hover(
function() {
uiDialogTitlebarClose.addClass('ui-state-hover');
},
function() {
uiDialogTitlebarClose.removeClass('ui-state-hover');
}
)
.focus(function() {
uiDialogTitlebarClose.addClass('ui-state-focus');
})
.blur(function() {
uiDialogTitlebarClose.removeClass('ui-state-focus');
})
.mousedown(function(ev) {
ev.stopPropagation();
})
.appendTo(uiDialogTitlebar);
var uiDialogTitlebarCloseText = (this.uiDialogTitlebarCloseText = $('<span/>'))
.addClass(
'ui-icon ' +
'ui-icon-closethick'
)
.text(options.closeText)
.appendTo(uiDialogTitlebarClose);
}
var uiDialogTitle = $('<span/>')
.addClass('ui-dialog-title')
.attr('id', titleId)
.html(title)
.prependTo(uiDialogTitlebar);
uiDialogTitlebar.find("*").add(uiDialogTitlebar).disableSelection();
},
destroy: function() {
this.element
.unbind('.dialog')
.removeData('dialog')
.removeClass('ui-dialog-content ui-widget-content')
.hide().appendTo('body');
(this.originalTitle && this.element.attr('title', this.originalTitle));
}
});
$.extend($.ssui.titlebar, {
version: "0.0.1",
options: {
title: '',
closeButton: false,
closeText: 'close'
},
uuid: 0,
getTitleId: function($el) {
return 'ui-dialog-title-' + ($el.attr('id') || ++this.uuid);
}
});
}(jQuery));

View File

@ -1,29 +0,0 @@
(function($) {
$('.ss-assetuploadfield').entwine({
onmatch: function() {
this._super();
// Hide the "second step" part until we're actually uploading
this.find('.ss-uploadfield-editandorganize').hide();
},
onunmatch: function() {
this._super();
},
onfileuploadadd: function(e) {
this.find('.ss-uploadfield-editandorganize').show();
},
onfileuploadstart: function(e) {
this.find('.ss-uploadfield-editandorganize').show();
}
});
$('.ss-uploadfield-view-allowed-extensions .toggle').entwine({
onclick: function(e) {
var allowedExt = this.closest('.ss-uploadfield-view-allowed-extensions'),
minHeightVal = this.closest('.ui-tabs-panel').height() + 20;
allowedExt.toggleClass('active');
allowedExt.find('.toggle-content').css('minHeight', minHeightVal);
}
});
}(jQuery));

View File

@ -1,11 +0,0 @@
(function ($) {
$(document).on('click', '.confirmedpassword .showOnClick a', function () {
var $container = $('.showOnClickContainer', $(this).parent());
$container.toggle('fast', function() {
$container.find('input[type="hidden"]').val($container.is(":visible") ? 1 : 0);
});
return false;
});
})(jQuery);

View File

@ -1,36 +0,0 @@
(function($) {
$.fn.extend({
ssDatepicker: function(opts) {
return $(this).each(function() {
if($(this).data('datepicker')) return; // already applied
$(this).siblings("button").addClass("ui-icon ui-icon-calendar");
var holder = $(this).parents('.field.date:first'),
config = $.extend(opts || {}, $(this).data(), $(this).data('jqueryuiconfig'), {});
if(!config.showcalendar) return;
if(config.locale && $.datepicker.regional[config.locale]) {
config = $.extend(config, $.datepicker.regional[config.locale], {});
}
if(config.min) config.minDate = $.datepicker.parseDate('yy-mm-dd', config.min);
if(config.max) config.maxDate = $.datepicker.parseDate('yy-mm-dd', config.max);
// Initialize and open a datepicker
// live() doesn't have "onmatch", and jQuery.entwine is a bit too heavyweight for this, so we need to do this onclick.
config.dateFormat = config.jquerydateformat;
$(this).datepicker(config);
});
}
});
$(document).on("click", ".field.date input.text,input.text.date", function() {
$(this).ssDatepicker();
if($(this).data('datepicker')) {
$(this).datepicker('show');
}
});
}(jQuery));

View File

@ -1,410 +0,0 @@
(function($){
$.entwine('ss', function($) {
$('.ss-gridfield').entwine({
/**
* @param {Object} Additional options for jQuery.ajax() call
* @param {successCallback} callback to call after reloading succeeded.
*/
reload: function(ajaxOpts, successCallback) {
var self = this, form = this.closest('form'),
focusedElName = this.find(':input:focus').attr('name'), // Save focused element for restoring after refresh
data = form.find(':input').serializeArray();
if(!ajaxOpts) ajaxOpts = {};
if(!ajaxOpts.data) ajaxOpts.data = [];
ajaxOpts.data = ajaxOpts.data.concat(data);
// Include any GET parameters from the current URL, as the view state might depend on it.
// For example, a list prefiltered through external search criteria might be passed to GridField.
if(window.location.search) {
ajaxOpts.data = window.location.search.replace(/^\?/, '') + '&' + $.param(ajaxOpts.data);
}
// For browsers which do not support history.pushState like IE9, ss framework uses hash to track
// the current location for PJAX, so for them we pass the query string stored in the hash instead
if(!window.history || !window.history.pushState){
if(window.location.hash && window.location.hash.indexOf('?') != -1){
ajaxOpts.data = window.location.hash.substring(window.location.hash.indexOf('?') + 1) + '&' + $.param(ajaxOpts.data);
}
}
form.addClass('loading');
$.ajax($.extend({}, {
headers: {"X-Pjax" : 'CurrentField'},
type: "POST",
url: this.data('url'),
dataType: 'html',
success: function(data) {
// Replace the grid field with response, not the form.
// TODO Only replaces all its children, to avoid replacing the current scope
// of the executing method. Means that it doesn't retrigger the onmatch() on the main container.
self.empty().append($(data).children());
// Refocus previously focused element. Useful e.g. for finding+adding
// multiple relationships via keyboard.
if(focusedElName) self.find(':input[name="' + focusedElName + '"]').focus();
// Update filter
if(self.find('.filter-header').length) {
var content;
if(ajaxOpts.data[0].filter=="show") {
content = '<span class="non-sortable"></span>';
self.addClass('show-filter').find('.filter-header').show();
} else {
content = '<button type="button" name="showFilter" class="ss-gridfield-button-filter trigger"></button>';
self.removeClass('show-filter').find('.filter-header').hide();
}
self.find('.sortable-header th:last').html(content);
}
form.removeClass('loading');
if(successCallback) successCallback.apply(this, arguments);
self.trigger('reload', self);
},
error: function(e) {
alert(ss.i18n._t('GRIDFIELD.ERRORINTRANSACTION'));
form.removeClass('loading');
}
}, ajaxOpts));
},
showDetailView: function(url) {
window.location.href = url;
},
getItems: function() {
return this.find('.ss-gridfield-item');
},
/**
* @param {String}
* @param {Mixed}
*/
setState: function(k, v) {
var state = this.getState();
state[k] = v;
this.find(':input[name="' + this.data('name') + '[GridState]"]').val(JSON.stringify(state));
},
/**
* @return {Object}
*/
getState: function() {
return JSON.parse(this.find(':input[name="' + this.data('name') + '[GridState]"]').val());
}
});
$('.ss-gridfield *').entwine({
getGridField: function() {
return this.closest('.ss-gridfield');
}
});
$('.ss-gridfield :button[name=showFilter]').entwine({
onclick: function(e) {
$('.filter-header')
.show('slow') // animate visibility
.find(':input:first').focus(); // focus first search field
this.closest('.ss-gridfield').addClass('show-filter');
this.parent().html('<span class="non-sortable"></span>');
e.preventDefault();
}
});
$('.ss-gridfield .ss-gridfield-item').entwine({
onclick: function(e) {
if($(e.target).closest('.action').length) {
this._super(e);
return false;
}
var editLink = this.find('.edit-link');
if(editLink.length) this.getGridField().showDetailView(editLink.prop('href'));
},
onmouseover: function() {
if(this.find('.edit-link').length) this.css('cursor', 'pointer');
},
onmouseout: function() {
this.css('cursor', 'default');
}
});
$('.ss-gridfield .action').entwine({
onclick: function(e){
var filterState='show'; //filterstate should equal current state.
// If the button is disabled, do nothing.
if (this.button('option', 'disabled')) {
e.preventDefault();
return;
}
if(this.hasClass('ss-gridfield-button-close') || !(this.closest('.ss-gridfield').hasClass('show-filter'))){
filterState='hidden';
}
this.getGridField().reload({data: [{name: this.attr('name'), value: this.val(), filter: filterState}]});
e.preventDefault();
}
});
/**
* Don't allow users to submit empty values in grid field auto complete inputs.
*/
$('.ss-gridfield .add-existing-autocompleter').entwine({
onbuttoncreate: function () {
var self = this;
this.toggleDisabled();
this.find('input[type="text"]').on('keyup', function () {
self.toggleDisabled();
});
},
onunmatch: function () {
this.find('input[type="text"]').off('keyup');
},
toggleDisabled: function () {
var $button = this.find('.ss-ui-button'),
$input = this.find('input[type="text"]'),
inputHasValue = $input.val() !== '',
buttonDisabled = $button.is(':disabled');
if ((inputHasValue && buttonDisabled) || (!inputHasValue && !buttonDisabled)) {
$button.button("option", "disabled", !buttonDisabled);
}
}
});
// Covers both tabular delete button, and the button on the detail form
$('.ss-gridfield .col-buttons .action.gridfield-button-delete, .cms-edit-form .Actions button.action.action-delete').entwine({
onclick: function(e){
if(!confirm(ss.i18n._t('TABLEFIELD.DELETECONFIRMMESSAGE'))) {
e.preventDefault();
return false;
} else {
this._super(e);
}
}
});
$('.ss-gridfield .action.gridfield-button-print').entwine({
UUID: null,
onmatch: function() {
this._super();
this.setUUID(new Date().getTime());
},
onunmatch: function() {
this._super();
},
onclick: function(e){
var btn = this.closest(':button'), grid = this.getGridField(),
form = this.closest('form'), data = form.find(':input.gridstate').serialize();;
// Add current button
data += "&" + encodeURIComponent(btn.attr('name')) + '=' + encodeURIComponent(btn.val());
// Include any GET parameters from the current URL, as the view
// state might depend on it.
// For example, a list prefiltered through external search criteria
// might be passed to GridField.
if(window.location.search) {
data = window.location.search.replace(/^\?/, '') + '&' + data;
}
// decide whether we should use ? or & to connect the URL
var connector = grid.data('url').indexOf('?') == -1 ? '?' : '&';
var url = $.path.makeUrlAbsolute(
grid.data('url') + connector + data,
$('base').attr('href')
);
var newWindow = window.open(url);
return false;
}
});
$('.ss-gridfield-print-iframe').entwine({
onmatch: function(){
this._super();
this.hide().bind('load', function() {
this.focus();
var ifWin = this.contentWindow || this;
ifWin.print();
});
},
onunmatch: function() {
this._super();
}
});
/**
* Prevents actions from causing an ajax reload of the field.
*
* Useful e.g. for actions which rely on HTTP response headers being
* interpreted natively by the browser, like file download triggers.
*/
$('.ss-gridfield .action.no-ajax').entwine({
onclick: function(e){
var self = this, btn = this.closest(':button'), grid = this.getGridField(),
form = this.closest('form'), data = form.find(':input.gridstate').serialize();
// Add current button
data += "&" + encodeURIComponent(btn.attr('name')) + '=' + encodeURIComponent(btn.val());
// Include any GET parameters from the current URL, as the view
// state might depend on it. For example, a list pre-filtered
// through external search criteria might be passed to GridField.
if(window.location.search) {
data = window.location.search.replace(/^\?/, '') + '&' + data;
}
// decide whether we should use ? or & to connect the URL
var connector = grid.data('url').indexOf('?') == -1 ? '?' : '&';
window.location.href = $.path.makeUrlAbsolute(
grid.data('url') + connector + data,
$('base').attr('href')
);
return false;
}
});
$('.ss-gridfield .action-detail').entwine({
onclick: function() {
this.getGridField().showDetailView($(this).prop('href'));
return false;
}
});
/**
* Allows selection of one or more rows in the grid field.
* Purely clientside at the moment.
*/
$('.ss-gridfield[data-selectable]').entwine({
/**
* @return {jQuery} Collection
*/
getSelectedItems: function() {
return this.find('.ss-gridfield-item.ui-selected');
},
/**
* @return {Array} Of record IDs
*/
getSelectedIDs: function() {
return $.map(this.getSelectedItems(), function(el) {return $(el).data('id');});
}
});
$('.ss-gridfield[data-selectable] .ss-gridfield-items').entwine({
onadd: function() {
this._super();
// TODO Limit to single selection
this.selectable();
},
onremove: function() {
this._super();
if (this.data('selectable')) this.selectable('destroy');
}
});
/**
* Catch submission event in filter input fields, and submit the correct button
* rather than the whole form.
*/
$('.ss-gridfield .filter-header :input').entwine({
onmatch: function() {
var filterbtn = this.closest('.fieldgroup').find('.ss-gridfield-button-filter'),
resetbtn = this.closest('.fieldgroup').find('.ss-gridfield-button-reset');
if(this.val()) {
filterbtn.addClass('filtered');
resetbtn.addClass('filtered');
}
this._super();
},
onunmatch: function() {
this._super();
},
onkeydown: function(e) {
// Skip reset button events, they should trigger default submission
if(this.closest('.ss-gridfield-button-reset').length) return;
var filterbtn = this.closest('.fieldgroup').find('.ss-gridfield-button-filter'),
resetbtn = this.closest('.fieldgroup').find('.ss-gridfield-button-reset');
if(e.keyCode == '13') {
var btns = this.closest('.filter-header').find('.ss-gridfield-button-filter');
var filterState='show'; //filterstate should equal current state.
if(this.hasClass('ss-gridfield-button-close')||!(this.closest('.ss-gridfield').hasClass('show-filter'))){
filterState='hidden';
}
this.getGridField().reload({data: [{name: btns.attr('name'), value: btns.val(), filter: filterState}]});
return false;
}else{
filterbtn.addClass('hover-alike');
resetbtn.addClass('hover-alike');
}
}
});
$(".ss-gridfield .relation-search").entwine({
onfocusin: function (event) {
this.autocomplete({
source: function(request, response){
var searchField = $(this.element);
var form = $(this.element).closest("form");
$.ajax({
headers: {
"X-Pjax" : 'Partial'
},
type: "GET",
url: $(searchField).data('searchUrl'),
data: encodeURIComponent(searchField.attr('name'))+'='+encodeURIComponent(searchField.val()),
success: function(data) {
response(JSON.parse(data));
},
error: function(e) {
alert(ss.i18n._t('GRIDFIELD.ERRORINTRANSACTION', 'An error occured while fetching data from the server\n Please try again later.'));
}
});
},
select: function(event, ui) {
$(this).closest(".ss-gridfield").find("#action_gridfield_relationfind").replaceWith(
'<input type="hidden" name="relationID" value="'+ui.item.id+'" id="relationID"/>'
);
var addbutton = $(this).closest(".ss-gridfield").find("#action_gridfield_relationadd");
if(addbutton.data('button')){
addbutton.button('enable');
}else{
addbutton.removeAttr('disabled');
}
}
});
}
});
$(".ss-gridfield .pagination-page-number input").entwine({
onkeydown: function(event) {
if(event.keyCode == 13) {
var newpage = parseInt($(this).val(), 10);
var gridfield = $(this).getGridField();
gridfield.setState('GridFieldPaginator', {currentPage: newpage});
gridfield.reload();
return false;
}
}
});
});
}(jQuery));

View File

@ -1,15 +0,0 @@
(function($) {
$(document).ready(function() {
$('ul.SelectionGroup input.selector').live('click', function() {
var li = $(this).closest('li');
li.addClass('selected');
var prev = li.prevAll('li.selected');
if(prev.length) prev.removeClass('selected');
var next = li.nextAll('li.selected');
if(next.length) next.removeClass('selected');
$(this).focus();
});
})
})(jQuery);

View File

@ -1,75 +0,0 @@
(function($){
$.entwine('ss', function($){
/**
* Lightweight wrapper around jQuery UI tabs for generic tab set-up
*/
$('.ss-tabset').entwine({
IgnoreTabState: false,
onadd: function() {
var hash = window.location.hash;
// Can't name redraw() as it clashes with other CMS entwine classes
this.redrawTabs();
if (hash !== '') {
this.openTabFromURL(hash);
}
this._super();
},
onremove: function() {
if(this.data('tabs')) this.tabs('destroy');
this._super();
},
redrawTabs: function() {
this.rewriteHashlinks();
this.tabs();
},
/**
* @func openTabFromURL
* @param {string} hash
* @desc Allows linking to a specific tab.
*/
openTabFromURL: function (hash) {
var $trigger;
// Make sure the hash relates to a valid tab.
$.each(this.find('.cms-panel-link'), function () {
// The hash in in the button's href and there is exactly one tab with that id.
if (this.href.indexOf(hash) !== -1 && $(hash).length === 1) {
$trigger = $(this);
return false; // break the loop
}
});
// If there's no tab, it means the hash is invalid, so do nothing.
if ($trigger === void 0) {
return;
}
// Switch to the correct tab when AJAX loading completes.
$(window).one('ajaxComplete', function () {
$trigger.click();
});
},
/**
* @func rewriteHashlinks
* @desc Ensure hash links are prefixed with the current page URL, otherwise jQuery interprets them as being external.
*/
rewriteHashlinks: function() {
$(this).find('ul a').each(function() {
if (!$(this).attr('href')) return;
var matches = $(this).attr('href').match(/#.*/);
if(!matches) return;
$(this).attr('href', document.location.href.replace(/#.*/, '') + matches[0]);
});
}
});
});
})(jQuery);

View File

@ -1,255 +0,0 @@
if(typeof(ss) == 'undefined') ss = {};
/*
* Lightweight clientside i18n implementation.
* Caution: Only available after DOM loaded because we need to detect the language
*
* For non-i18n stub implementation, see framework/javascript/i18nx.js
*
* Based on jQuery i18n plugin: 1.0.0 Feb-10-2008
*
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
* Based on 'javascript i18n that almost doesn't suck' by markos
* http://markos.gaivo.net/blog/?p=100
*/
ss.i18n = {
currentLocale: null,
defaultLocale: 'en_US',
lang: {},
inited: false,
init: function() {
if(this.inited) return;
this.currentLocale = this.detectLocale();
this.inited = true;
},
/**
* set_locale()
* Set locale in long format, e.g. "de_AT" for Austrian German.
* @param string locale
*/
setLocale: function(locale) {
this.currentLocale = locale;
},
/**
* getLocale()
* Get locale in long format. Falls back to i18n.defaut_locale.
* @return string
*/
getLocale: function() {
return (this.currentLocale) ? this.currentLocale : this.defaultLocale;
},
/**
* _()
* The actual translation function. Looks the given string up in the
* dictionary and returns the translation if one exists. If a translation
* is not found, returns the original word
*
* @param string entity A "long" locale format, e.g. "de_DE" (Required)
* @param string fallbackString (Required)
* @param int priority (not used)
* @param string context Give translators context for the string
* @return string : Translated word
*
*/
_t: function (entity, fallbackString, priority, context) {
this.init();
var langName = this.getLocale().replace(/_[\w]+/i, '');
var defaultlangName = this.defaultLocale.replace(/_[\w]+/i, '');
if (this.lang && this.lang[this.getLocale()] && this.lang[this.getLocale()][entity]) {
return this.lang[this.getLocale()][entity];
} else if (this.lang && this.lang[langName] && this.lang[langName][entity]) {
return this.lang[langName][entity];
} else if (this.lang && this.lang[this.defaultLocale] && this.lang[this.defaultLocale][entity]) {
return this.lang[this.defaultLocale][entity];
} else if (this.lang && this.lang[defaultlangName] && this.lang[defaultlangName][entity]) {
return this.lang[defaultlangName][entity];
} else if(fallbackString) {
return fallbackString;
} else {
return '';
}
},
/**
* Add entities to a dictionary. If a dictionary doesn't
* exist for this locale, its automatically created.
* Existing entities are overwritten.
*
* @param string locale
* @param Object dict
*/
addDictionary: function(locale, dict) {
if(!this.lang[locale]) this.lang[locale] = {};
for(entity in dict) {
this.lang[locale][entity] = dict[entity];
}
},
/**
* Get dictionary for a specific locale.
*
* @param string locale
*/
getDictionary: function(locale) {
return this.lang[locale];
},
/**
* stripStr()
*
* @param string str : The string to strip
* @return string result : Stripped string
*
*/
stripStr: function(str) {
return str.replace(/^\s*/, "").replace(/\s*$/, "");
},
/**
* stripStrML()
*
* @param string str : The multi-line string to strip
* @return string result : Stripped string
*
*/
stripStrML: function(str) {
// Split because m flag doesn't exist before JS1.5 and we need to
// strip newlines anyway
var parts = str.split('\n');
for (var i=0; i<parts.length; i++)
parts[i] = stripStr(parts[i]);
// Don't join with empty strings, because it "concats" words
// And strip again
return stripStr(parts.join(" "));
},
/**
* Substitutes %s with parameters
* given in list. %%s is used to escape %s.
*
* @param string S : The string to perform the substitutions on.
* @return string The new string with substitutions made
*/
sprintf: function(S) {
if (arguments.length == 1) return S;
var args = [],
len = arguments.length,
index = 0,
regx = new RegExp('(.?)(%s)', 'g'),
result;
for (var i=1; i<len; ++i) {
args.push(arguments[i]);
};
result = S.replace(regx, function(match, subMatch1, subMatch2, offset, string){
if (subMatch1 == '%') return match; // skip %%s
return subMatch1 + args[index++];
});
return result;
},
/**
* Substitutes variables with a list of injections.
*
* @param string S : The string to perform the substitutions on.
* @param object map : An object with the substitions map e.g. {var: value}
* @return string The new string with substitutions made
*/
inject: function(S, map) {
var regx = new RegExp("\{([A-Za-z0-9_]*)\}", "g"),
result;
result = S.replace(regx, function(match, key, offset, string){
return (map[key]) ? map[key] : match;
});
return result;
},
/**
* Detect document language settings by looking at <meta> tags.
* If no match is found, returns this.defaultLocale.
*
* @todo get by <html lang=''> - needs modification of SSViewer
*
* @return string Locale in mixed lowercase/uppercase format suitable
* for usage in ss.i18n.lang arrays (e.g. 'en_US').
*/
detectLocale: function() {
var rawLocale;
var detectedLocale;
// get by container tag
rawLocale = jQuery('html').attr('lang') || jQuery('body').attr('lang');
// get by meta
if(!rawLocale) {
var metas = document.getElementsByTagName('meta');
for(var i=0; i<metas.length; i++) {
if(metas[i].attributes['http-equiv'] && metas[i].attributes['http-equiv'].nodeValue.toLowerCase() == 'content-language') {
rawLocale = metas[i].attributes['content'].nodeValue;
}
}
}
// fallback to default locale
if(!rawLocale) rawLocale = this.defaultLocale;
var rawLocaleParts = rawLocale.match(/([^-|_]*)[-|_](.*)/);
// get locale (e.g. 'en_US') from common name (e.g. 'en')
// by looking at ss.i18n.lang tables
if(rawLocale.length == 2) {
for(compareLocale in ss.i18n.lang) {
if(compareLocale.substr(0,2).toLowerCase() == rawLocale.toLowerCase()) {
detectedLocale = compareLocale;
break;
}
}
} else if(rawLocaleParts) {
detectedLocale = rawLocaleParts[1].toLowerCase() + '_' + rawLocaleParts[2].toUpperCase();
}
return detectedLocale;
},
/**
* Attach an event listener to the given object.
* Modeled after behaviour.js, but externalized
* to keep the i18n library standalone for now.
*/
addEvent: function(obj, evType, fn, useCapture){
if (obj.addEventListener){
obj.addEventListener(evType, fn, useCapture);
return true;
} else if (obj.attachEvent){
var r = obj.attachEvent("on"+evType, fn);
return r;
} else {
alert("Handler could not be attached");
}
}
};
ss.i18n.addEvent(window, "load", function() {
ss.i18n.init();
});

View File

@ -1,53 +0,0 @@
if(typeof(ss) == 'undefined') ss = {};
/**
* Stub implementation for ss.i18n code.
* Use instead of framework/javascript/i18n.js
* if you want to use any SilverStripe javascript
* without internationalization support.
*/
ss.i18n = {
currentLocale: 'en_US',
defaultLocale: 'en_US',
_t: function (entity, fallbackString, priority, context) {
return fallbackString;
},
sprintf: function(S) {
if (arguments.length == 1) return S;
var args = [],
len = arguments.length,
index = 0,
regx = new RegExp('(.?)(%s)', 'g'),
result;
for (var i=1; i<len; ++i) {
args.push(arguments[i]);
};
result = S.replace(regx, function(match, subMatch1, subMatch2, offset, string){
if (subMatch1 == '%') return match; // skip %%s
return subMatch1 + args[index++];
});
return result;
},
inject: function(S, map) {
var regx = new RegExp("\{([A-Za-z0-9_]*)\}", "g"),
result;
result = S.replace(regx, function(match, key, offset, string){
return (map[key]) ? map[key] : match;
});
return result;
},
// stub methods
addDictionary: function() {},
getDictionary: function() {}
};

View File

@ -1,212 +0,0 @@
/*
* Default CSS for tree-view
*/
ul.tree{
width: auto;
padding-left: 0;
margin-left: 0;
}
ul.tree img {
border: none;
}
ul.tree, ul.tree ul {
padding-left: 0;
}
ul.tree ul {
margin-left: 16px;
}
ul.tree li.closed ul {
display: none;
}
ul.tree li {
list-style: none;
background: url(images/i-repeater.gif) repeat-y 1 0;
display: block;
width: auto;
}
ul.tree li.last {
list-style: none;
background-image: none;
}
/* Span-A: I/L/I glpyhs */
ul.tree span.a {
background: url(images/t.gif) no-repeat 0 50%;
display: block;
}
ul.tree .a.last {
background: url(images/l.gif) no-repeat 0 50%;
}
/* Span-B: Plus/Minus icon */
ul.tree span.b {
}
ul.tree span.a.children span.b {
display: inline-block;
background: url(images/minus.gif) no-repeat 0 50%;
cursor: pointer;
}
ul.tree li.closed span.a span.b, ul.tree span.a.unexpanded span.b {
display: inline-block;
background: url(images/plus.gif) no-repeat 0 50%;
cursor: pointer;
}
/* Span-C: Spacing and extending tree line below the icon */
ul.tree span.c {
margin-left: 16px;
}
ul.tree span.a.children span.c, ul.tree span.a.spanClosed span.c {
background: url(images/i-bottom.gif) no-repeat 0 50%;
}
ul.tree span.a.spanClosed span.c, ul.tree span.a.unexpanded span.c {
background-image: none;
}
/* Anchor tag: Page icon */
ul.tree span.c {
white-space: nowrap;
}
ul.tree a {
display: inline-block; /* IE needs this */
white-space: pre;
overflow: hidden;
padding: 3px 0 1px 19px;
line-height: 16px;
background: url(images/page-file.png) no-repeat 0 50%;
background-position: 0 50% !important;
text-decoration: none;
outline: none;
font-size: 11px;
}
ul.tree a * {
font-size: 11px;
}
ul.tree a:hover {
text-decoration: underline;
}
ul.tree span.a.spanClosed a, ul.tree span.a.Folder a {
background-image: url(images/page-closedfolder.png);
}
ul.tree span.a.children a {
background-image: url(images/page-openfolder.png);
}
/* Unformatted tree */
ul.tree.unformatted li {
background-image: none;
padding-left: 16px;
}
ul.tree.unformatted li li {
background-image: none;
padding-left: 0;
}
/*
Hover / Link tags
*/
ul.tree a:hover{
text-decoration : none;
}
ul.tree span.a {
cursor: pointer;
}
/*
* Divs, by default store vertically aligned data
*/
/* As inside DIVs should be treated normally */
ul.tree div a {
padding: 0;
background-image: none;
min-height: 0;
height: auto;
}
ul.tree li a:link,
ul.tree li a:hover,
ul.tree li a:visited {
color: #111;
}
/*
* Drag and drop styling
*/
ul.tree div.droppable {
float: none;
margin: -7px 0px -7px 16px;
height: 10px;
font-size: 1px;
z-index: 1000;
}
html > body ul.tree div.droppable {
margin: -5px 0px -5px 16px;
}
ul.tree div.droppable.dragOver {
background: url(images/insertBetween.gif) no-repeat 50% 0;
}
ul.tree a.dragOver, ul.tree li.dragOver a, ul.tree li.dragOver li.dragOver a {
border: 3px solid #0074C6;
margin: -3px;
}
ul.tree li.dragOver li a {
border-style: none;
margin: 0;
}
/**
* Multiselect
*/
ul.tree span.a.current {
font-weight: bold;
background-color: #EEEEFF !important;
border-top: 1px #CCCCFF solid;
border-bottom: 1px #CCCCFF solid;
/* these push the highlight out to the left of the window */
margin-left: -100px;
padding-left: 100px;
background-position: 100px 50%;
/*position: relative;*/
}
ul.tree span.a.loading span.b span.c a {
background-image: url(../../images/network-save.gif) !important;
margin-left: -3px;
padding-left: 21px;
background-position : 2px 2px;
}
ul.tree.multiselect span.a span.b a {
background-image: url(../../images/tickbox-unticked.gif) !important;
}
ul.tree.multiselect span.a.nodelete span.b a {
background-image: url(../../images/tickbox-canttick.gif) !important;
}
ul.tree.multiselect span.a.treeloading span.b a {
background-image: url(../../images/tickbox-greyedout.gif) !important;
}
ul.tree.multiselect span.a.failed span.b a {
background-image: url(../../images/tickbox-fail.gif) !important;
}
ul.tree.multiselect span.a.selected span.b span.c a {
background-image: url(../../images/tickbox-ticked.gif) !important;
}
/* Span-B: Plus/Minus icon */
ul.tree.multiselect li.selected span.a.children span.b,
ul.tree.multiselect li.selected span.a.unexpanded span.b {
background-image: none;
cursor: default;
}

View File

@ -1,925 +0,0 @@
/*
* Content-separated javascript tree widget
*
* Usage:
* behaveAs(someUL, Tree)
* OR behaveAs(someUL, DraggableTree)
*
* Extended by Steven J. DeRose, deroses@mail.nih.gov, sderose@acm.org.
*
* INPUT REQUIREMENTS:
* Put class="tree" on topmost UL(s).
* Can put class="closed" on LIs to have them collapsed on startup.
*
* The structure we build is:
* li class="children last closed" <=== original li from source
* children: there's a UL child
* last: no following LI
* closed: is collapsed (may be in src)
* span class="a children spanClosed" <=== contains children before UL
* children: there's a UL (now a sib)
* spanClosed: is collapsed
* span class="b" <=== +/- click is caught here
* span class="c" <=== for spacing and lines
* a href="..." <=== original pre-UL stuff (e.g., a)
* ul...
*
*/
Tree = Class.create();
Tree.prototype = {
/*
* Initialise a tree node, converting all its LIs appropriately.
* This means go through all li children, and move the content of each
* (before any UL child) down into 3 intermediate spans, classes a/b/c.
*/
initialize: function(options) {
this.isDraggable = false;
var i,li;
this.options = options ? options : {};
if(!this.options.tree) this.options.tree = this;
this.tree = this.options.tree;
// Set up observer
if(this == this.tree) Observable.applyTo(this);
// Find all LIs to process
// Don't let it re-do a node it's already done.
for(i=0;i<this.childNodes.length;i++) {
if(this.childNodes[i].tagName && this.childNodes[i].tagName.toLowerCase() == 'li' &&
!(this.childNodes[i].childNodes[0] &&
this.childNodes[i].childNodes[0].attributes &&
this.childNodes[i].childNodes[0].attributes["class"] &&
this.childNodes[i].childNodes[0].attributes["class"] == "a")) {
li = this.childNodes[i];
this.castAsTreeNode(li);
// If we've added a DIV to this node, then increment i;
while(this.childNodes[i].tagName.toLowerCase() != "li") i++;
}
}
// Not sure what following line is really doing for us....
this.className = this.className.replace(/ ?unformatted ?/, ' ');
if(li) {
li.addNodeClass('last');
//li.addNodeClass('closed');
if(this.parentNode.tagName.toLowerCase() == "li") {
this.treeNode = this.parentNode;
}
return true;
} else {
return false;
}
},
destroy: function() {
this.tree = null;
this.treeNode = null;
if(this.options) this.options.tree = null;
this.options = null;
},
/**
* Convert the given <li> tag into a suitable tree node
*/
castAsTreeNode: function(li) {
behaveAs(li, TreeNode, this.options);
},
getIdxOf : function(el) {
if(!el.treeNode) el.treeNode = el;
// Special case for TreeMultiselectField
if(el.treeNode.id.match(/^selector-([^-]+)-([0-9]+)$/)) return RegExp.$2;
// Other case for LHS tree of CMS
if(el.treeNode.id.match(/([^-]+)-(.+)$/)) return RegExp.$1;
else return el.treeNode.id;
},
childTreeNodes: function() {
var i,item, children = [];
for(i=0;item=this.childNodes[i];i++) {
if(item.tagName && item.tagName.toLowerCase() == 'li') children.push(item);
}
return children;
},
hasChildren: function() {
return this.childTreeNodes().length > 0;
},
/**
* Turn a normal tree into a draggable one.
*/
makeDraggable: function() {
this.isDraggable = true;
var i,item,x;
var trees = this.getElementsByTagName('ul');
for(x in DraggableTree.prototype) this[x] = DraggableTree.prototype[x];
DraggableTree.prototype.setUpDragability.apply(this);
var nodes = this.getElementsByTagName('li');
for(i=0;item=nodes[i];i++) {
for(x in DraggableTreeNode.prototype) item[x] = DraggableTreeNode.prototype[x];
}
for(i=0;item=trees[i];i++) {
for(x in DraggableTree.prototype) item[x] = DraggableTree.prototype[x];
}
for(i=0;item=nodes[i];i++) {
DraggableTreeNode.prototype.setUpDragability.apply(item);
}
for(i=0;item=trees[i];i++) {
DraggableTree.prototype.setUpDragability.apply(item);
}
},
/**
* Add the given child node to this tree node.
* If 'before' is specified, then it will be inserted before that.
*/
appendTreeNode : function(child, before) {
if(!child) return;
// Remove from the old parent node - this will ensure that the classes of the old tree
// item are updated accordingly
if(child && child.parentTreeNode) {
var oldParent = child.parentTreeNode;
oldParent.removeTreeNode(child);
}
var lastNode, i, holder = this;
if(lastNode = this.lastTreeNode()) lastNode.removeNodeClass('last');
// Do the actual moving
if(before) {
child.removeNodeClass('last');
if(holder != before.parentNode) {
throw("TreeNode.appendTreeNode: 'before' not contained within the holder");
holder.appendChild(child);
} else {
holder.insertBefore(child, before);
}
} else {
holder.appendChild(child);
}
if(this.parentNode && this.parentNode.fixDragHelperDivs) this.parentNode.fixDragHelperDivs();
if(oldParent && oldParent.fixDragHelperDivs) oldParent.fixDragHelperDivs();
// Update the helper classes
if(this.parentNode && this.parentNode.tagName.toLowerCase() == 'li') {
if(this.parentNode.className.indexOf('closed') == -1) this.parentNode.addNodeClass('children');
this.lastTreeNode().addNodeClass('last');
}
// Update the helper variables
if(this.parentNode.tagName.toLowerCase() == 'li') child.parentTreeNode = this.parentNode;
else child.parentTreeNode = null;
if(this.isDraggable) {
for(x in DraggableTreeNode.prototype) child[x] = DraggableTreeNode.prototype[x];
DraggableTreeNode.prototype.setUpDragability.apply(child);
}
},
lastTreeNode : function() {
var i, holder = this;
for(i=holder.childNodes.length-1;i>=0;i--) {
if(holder.childNodes[i].tagName && holder.childNodes[i].tagName.toLowerCase() == 'li') return holder.childNodes[i];
}
},
/**
* Remove the given child node from this tree node.
*/
removeTreeNode : function(child) {
// Remove the child
var holder = this;
try { holder.removeChild(child); } catch(er) { }
// Look for remaining children
var i, hasChildren = false;
for(i=0;i<holder.childNodes.length;i++) {
if(holder.childNodes[i].tagName && holder.childNodes[i].tagName.toLowerCase() == "li") {
hasChildren = true;
break;
}
}
// Update the helper classes accordingly
if(!hasChildren) this.removeNodeClass('children');
else this.lastTreeNode().addNodeClass('last');
// Update the helper variables
if(child.parentTreeNode == this.parentNode) {
child.parentTreeNode = null;
}
},
open: function() {
},
expose: function() {
},
addNodeClass : function(className) {
if( this.parentNode.tagName.toLowerCase() == 'li' )
this.parentNode.addNodeClass(className);
},
removeNodeClass : function(className) {
if( this.parentNode.tagName.toLowerCase() == 'li' )
this.parentNode.removeNodeClass(className);
}
}
TreeNode = Class.create();
TreeNode.prototype = {
initialize: function(options) {
var spanA, spanB, spanC;
var startingPoint, stoppingPoint, childUL;
var j;
// Basic hook-ups
var li = this;
this.options = options ? options : {};
this.tree = this.options.tree;
if(!this.ajaxExpansion && this.options.ajaxExpansion)
this.ajaxExpansion = this.options.ajaxExpansion;
if(this.options.getIdx)
this.getIdx = this.options.getIdx;
// Get this.recordID from the last "-" separated chunk of the id HTML attribute
// eg: <li id="treenode-6"> would give a recordID of 6
if(this.id && this.id.match(/([^-]+)-(.+)$/))
this.recordID = RegExp.$1;
// Create our extra spans
spanA = document.createElement('span');
spanB = document.createElement('span');
spanC = document.createElement('span');
spanA.appendChild(spanB);
spanB.appendChild(spanC);
spanA.className = 'a ' + li.className.replace('closed','spanClosed');
spanB.className = 'b';
spanB.onclick = TreeNode_bSpan_onclick;
spanC.className = 'c';
this.castAsSpanA(spanA);
// Add +/- icon to select node that has children
if (li.hasChildren() && li.className.indexOf('current') > -1) {
li.className = li.className + ' children';
spanA.className = spanA.className + ' children';
}
// Find the UL within the LI, if it exists
stoppingPoint = li.childNodes.length;
startingPoint = 0;
childUL = null;
for(j=0;j<li.childNodes.length;j++) {
// Find last div before first ul (unnecessary in our usage)
/*
if(li.childNodes[j].tagName && li.childNodes[j].tagName.toLowerCase() == 'div') {
startingPoint = j + 1;
continue;
}
*/
if(li.childNodes[j].tagName && li.childNodes[j].tagName.toLowerCase() == 'ul') {
childUL = li.childNodes[j];
stoppingPoint = j;
break;
}
}
// Move all the nodes up until that point into spanC
for(j=startingPoint;j<stoppingPoint;j++) {
/* Use [startingPoint] every time, because the appentChild
removes the node, so it then points to the next one. */
spanC.appendChild(li.childNodes[startingPoint]);
}
// Insert the outermost extra span into the tree
if(li.childNodes.length > startingPoint) li.insertBefore(spanA, li.childNodes[startingPoint]);
else li.appendChild(spanA);
// Create appropriate node references;
if(li.parentNode && li.parentNode.parentNode && li.parentNode.parentNode.tagName.toLowerCase() == 'li') {
li.parentTreeNode = li.parentNode.parentNode;
}
li.aSpan = spanA;
li.bSpan = spanB;
li.cSpan = spanC;
li.treeNode = spanA.treeNode = spanB.treeNode = spanC.treeNode = li;
var aTag = spanC.getElementsByTagName('a')[0];
if(aTag) {
aTag.treeNode = li;
li.aTag = aTag;
} else {
throw("Tree creation: A tree needs <a> tags inside the <li>s to work properly.");
}
aTag.onclick = TreeNode_aTag_onclick.bindAsEventListener(aTag);
// Process the children
if(childUL != null) {
if(this.castAsTree(childUL)) { /* ***** RECURSE ***** */
if(this.className.indexOf('closed') == -1) {
this.addNodeClass('children');
}
}
} else {
this.removeNodeClass('closed');
}
this.setIconByClass();
},
destroy: function() {
// Debug.show(this);
this.tree = null;
this.treeNode = null;
this.parentTreeNode = null;
if(this.options) this.options.tree = null;
this.options = null;
if(this.aTag) {
this.aTag.treeNode = null;
this.aTag.onclick = null;
}
if(this.aSpan) {
this.aSpan.treeNode = null;
this.aSpan.onmouseover = null;
this.aSpan.onmouseout = null;
}
if(this.bSpan) {
this.bSpan.treeNode = null;
this.bSpan.onclick = null;
}
if(this.cSpan) this.cSpan.treeNode = null;
this.aSpan = null;
this.bSpan = null;
this.cSpan = null;
this.aTag = null;
},
/**
* Cast the given span as the <span class="a"> item for this tree.
*/
castAsSpanA: function(spanA) {
var x;
for(x in TreeNode_SpanA) spanA[x] = TreeNode_SpanA[x];
},
/**
* Cast the child <ul> as a tree
*/
castAsTree: function(childUL) {
return behaveAs(childUL, Tree, this.options);
},
/**
* Triggered from clicks on spans of class b, the +/- buttons.
* Closed is represented by adding class close to the LI, and
* class spanClose to spanA.
* Pass 'force' as "open" or "close" to force it to that state,
* otherwise it toggles.
*/
toggle : function(force) {
if(this.treeNode.wasDragged || this.treeNode.anchorWasClicked) {
this.treeNode.wasDragged = false;
this.treeNode.anchorWasClicked = false;
return;
}
/* Note: It appears the 'force' parameter is no longer used. Here is old code that used it:
if( force == "open"){
treeOpen( topSpan, el )
}
else if( force == "close" ){
treeClose( topSpan, el )
}
*/
if(this.hasChildren() || this.className.match(/(^| )unexpanded($| )/)) {
if(this.className.match(/(^| )closed($| )/) || this.className.match(/(^| )unexpanded($| )/)) this.open();
else this.close();
}
},
open : function () {
// Normal tree node
if(Element.hasClassName(this, 'unexpanded') && !this.hasChildren()) {
if(this.ajaxExpansion) this.ajaxExpansion();
}
if(!this.className.match(/(^| )closed($| )/)) return;
this.removeNodeClass('closed');
this.removeNodeClass('unexpanded');
},
close : function () {
this.addNodeClass('closed');
},
expose : function() {
if(this.parentTreeNode) {
this.parentTreeNode.open();
this.parentTreeNode.expose();
}
},
setIconByClass: function() {
if(typeof _TREE_ICONS == 'undefined') return;
var classes = this.className.split(/\s+/);
var obj = this;
classes.each(function(className) {
var className = className.replace(/class-/, '');
if(_TREE_ICONS[className]) {
obj.fileIcon = _TREE_ICONS[className].fileIcon;
obj.openFolderIcon = _TREE_ICONS[className].openFolderIcon;
obj.closedFolderIcon = _TREE_ICONS[className].closedFolderIcon;
throw $break;
} else if(className == "Page") {
obj.fileIcon = null;
obj.openFolderIcon = null;
obj.closedFolderIcon = null;
}
});
this.updateIcon();
},
updateIcon: function() {
var icon;
if(this.closedFolderIcon && this.className.indexOf('closed') != -1) {
icon = this.closedFolderIcon;
} else if(this.openFolderIcon && this.className.indexOf('children') != -1) {
icon = this.openFolderIcon;
} else if(this.fileIcon) {
icon = this.fileIcon;
}
if(icon) this.aTag.style.background = "url(" +icon + ") no-repeat";
else this.aTag.style.backgroundImage = "";
},
/**
* Add the given child node to this tree node.
* If 'before' is specified, then it will be inserted before that.
*/
appendTreeNode : function(child, before) {
this.treeNodeHolder().appendTreeNode(child, before);
},
treeNodeHolder : function(performCast) {
if(performCast == null) performCast = true;
var uls = this.getElementsByTagName('ul');
if(uls.length > 0) return uls[0];
else {
var ul = document.createElement('ul');
this.appendChild(ul);
if(performCast) this.castAsTree(ul);
return ul;
}
},
hasChildren: function() {
var uls = this.getElementsByTagName('ul');
if(uls.length > 0) {
var i,item;
for(i=0;item=uls[0].childNodes[i];i++) {
if(item.tagName && item.tagName.toLowerCase() == 'li') return true;
}
}
return false;
},
/**
* Remove the given child node from this tree node.
*/
removeTreeNode : function(child) {
// Remove the child
var holder = this.treeNodeHolder();
try { holder.removeChild(child); } catch(er) { }
// Look for remaining children
var i, hasChildren = false;
for(i=0;i<holder.childNodes.length;i++) {
if(holder.childNodes[i].tagName && holder.childNodes[i].tagName.toLowerCase() == "li") {
hasChildren = true;
break;
}
}
// Update the helper classes accordingly
if(!hasChildren) this.removeNodeClass('children');
else this.lastTreeNode().addNodeClass('last');
// Update the helper variables
child.parentTreeNode = null;
},
lastTreeNode : function() {
return this.treeNodeHolder().lastTreeNode();
},
firstTreeNode : function() {
var i, holder = this.treeNodeHolder();
for(i=0;i<holder.childNodes.length;i++) {
if(holder.childNodes[i].tagName && holder.childNodes[i].tagName.toLowerCase() == 'li') return holder.childNodes[i];
}
},
addNodeClass : function(className) {
if(Element && Element.addClassName) {
Element.addClassName(this, className);
if(className == 'closed') Element.removeClassName(this, 'children');
this.aSpan.className = 'a ' + this.className.replace('closed','spanClosed');
if(className == 'children' || className == 'closed') this.updateIcon();
}
},
removeNodeClass : function(className) {
if(Element && Element.removeClassName) {
Element.removeClassName(this, className);
if(className == 'closed' && this.hasChildren()) Element.addClassName(this, 'children');
this.aSpan.className = 'a ' + this.className.replace('closed','spanClosed');
if(className == 'children' || className == 'closed') this.updateIcon();
}
},
getIdx : function() {
if(this.id.match(/([^-]+)-(.+)$/)) return RegExp.$2;
else return this.id;
},
getTitle: function() {
return this.aTag.innerHTML;
},
installSubtree : function(response) {
var ul = this.treeNodeHolder(false);
ul.innerHTML = response.responseText;
ul.appendTreeNode = null;
this.castAsTree(ul);
/*
var i,lis = ul.childTreeNodes();
for(i=0;i<lis.length;i++) {
this.tree.castAsTreeNode(lis[i]);
}
*/
// Cued new nodes are nodes added while we were waiting for the expansion to finish
if(ul.cuedNewNodes) {
var i;
for(i=0;i<ul.cuedNewNodes.length;i++) {
ul.appendTreeNode(ul.cuedNewNodes[i]);
}
ul.cuedNewNodes = null;
}
this.removeNodeClass('closed');
this.addNodeClass('children');
this.removeNodeClass('loading');
this.removeNodeClass('unexpanded');
}
}
/* Close or Open all the trees, at beginning or on request. sjd. */
function treeCloseAll() {
var candidates = document.getElementsByTagName('li');
for (var i=0;i<candidates.length;i++) {
var aSpan = candidates[i].childNodes[0];
if(aSpan.childNodes[0] && aSpan.childNodes[0].className == "b") {
if (!aSpan.className.match(/spanClosed/) && candidates[i].id != 'record-0' ) {
aSpan.childNodes[0].onclick();
}
}
}
}
function treeOpenAll() {
var candidates = document.getElementsByTagName('li');
for (var i=0;i<candidates.length;i++) {
var aSpan = candidates[i].childNodes[0];
if(aSpan.childNodes[0] && aSpan.childNodes[0].className == "b") {
if (aSpan.className.match(/spanClosed/)) {
aSpan.childNodes[0].onclick();
}
}
}
}
TreeNode_aTag_onclick = function(event) {
Event.stop(event);
jQuery(this.treeNode.tree).trigger('nodeclicked', {node: this.treeNode});
if(!this.treeNode.tree || this.treeNode.tree.notify('NodeClicked', this.treeNode)) {
if(this.treeNode.options.onselect) {
return this.treeNode.options.onselect.apply(this.treeNode, [event]);
} else if(this.treeNode.onselect) {
return this.treeNode.onselect();
}
}
return false;
}
TreeNode_bSpan_onclick = function() {
this.treeNode.toggle();
};
TreeNode_SpanA = {
onmouseover : function(event) {
this.parentNode.addNodeClass('over');
},
onmouseout : function(event) {
this.parentNode.removeNodeClass('over');
}
}
//-----------------------------------------------------------------------------------------------//
DraggableTree = Class.extend('Tree');
DraggableTree.prototype = {
initialize: function(options) {
this.Tree.initialize(options);
this.setUpDragability();
},
setUpDragability: function() {
this.isDraggable = true;
this.allDragHelpers = [];
if(this.parentNode.tagName.toLowerCase() == "li") {
this.treeNode = this.parentNode;
if(this.treeNode.hasChildren()) {
this.treeNode.createDragHelper();
}
}
},
/**
* Turn a draggable tree into a normal one.
*/
stopBeingDraggable: function() {
// this.parentNode.destroy();
this.isDraggable = false;
var i,item,nodes = this.getElementsByTagName('li');
for(i=0;item=nodes[i];i++) {
item.destroyDraggable();
}
for(i=0;item=this.allDragHelpers[i];i++) {
Droppables.remove(item);
if(item.parentNode){
item.parentNode.removeChild(item);
}
}
this.allDragHelpers = [];
},
/**
* Convert the given <li> tag into a suitable tree node
*/
castAsTreeNode: function(li) {
behaveAs(li, DraggableTreeNode, this.options);
}
}
DraggableTreeNode = Class.extend('TreeNode');
DraggableTreeNode.prototype = {
initialize: function(options) {
this.TreeNode.initialize(options);
this.setUpDragability();
},
setUpDragability: function() {
// Set up drag and drop
this.draggableObj = new Draggable(this, TreeNodeDragger);
//if(!this.dropperOptions || this.dropperOptions.accept != 'none')
Droppables.add(this.aTag, this.dropperOptions ? Object.extend(this.dropperOptions, TreeNodeDropper) : TreeNodeDropper);
// Add before DIVs to be Droppable items
if(this.parentTreeNode && this.parentTreeNode.createDragHelper){
this.parentTreeNode.createDragHelper(this);
}
if(this.hasChildren() && this.parentNode.tagName.toLowerCase() == "li") {
this.treeNode = this.parentNode;
// this.treeNode.createDragHelper();
}
// Fix up the <a> click action
this.aTag._onclick_before_draggable = this.aTag.onclick;
this.aTag.baseClick = this.aTag.onclick;
if(this.options.onParentChanged) this.onParentChanged = this.options.onParentChanged;
if(this.options.onOrderChanged) this.onOrderChanged = this.options.onOrderChanged;
},
/**
* Remove all the draggy stuff
*/
destroyDraggable: function() {
Droppables.remove(this.aTag);
this.aTag.onclick = this.aTag._onclick_before_draggable;
if(this.draggableObj) {
this.draggableObj.destroy();
this.draggableObj = null;
}
},
/*
this was commented out because SiteTreeNode takes care of it instead
castAsTree: function(childUL) {
// Behaving as DraggableTree directly doesn't load in expansion behaviours
behaveAs(childUL, Tree, this.options);
childUL.makeDraggable();
},
*/
/**
* Rebuild the "Drag Helper DIVs" that sit around each tree node within this node
*/
fixDragHelperDivs : function() {
var i, holder = this.treeNodeHolder();
// This variable toggles between div & li
var lastDiv, expecting = "div";
for(i=0;i<holder.childNodes.length;i++) {
if(holder.childNodes[i].tagName) {
if(holder.childNodes[i].tagName.toLowerCase() == "div") lastDiv = holder.childNodes[i];
// alert(i + ': ' + expecting + ', ' + holder.childNodes[i].tagName);
if(expecting != holder.childNodes[i].tagName.toLowerCase()) {
if(expecting == "div") {
this.createDragHelper(holder.childNodes[i]);
} else {
holder.removeChild(holder.childNodes[i]);
}
i--;
} else {
// Toggle expecting
expecting = (expecting == "div") ? "li" : "div";
}
}
}
// If we were left looking for an li, remove the last div
// if(expecting == "li") holder.removeChild(lastDiv);
// If we were left looking for a div, add one at the end
if(expecting == "div") this.createDragHelper();
},
/**
* Create a drag helper within this item.
* It will be inserted to the end, or before the 'before' element if that is given.
*/
createDragHelper : function(before) {
// Create the node
var droppable = document.createElement('div');
droppable.className = "droppable";
droppable.treeNode = this;
this.dragHelper = droppable;
this.tree.allDragHelpers[this.tree.allDragHelpers.length] = this.dragHelper;
// Insert into the DOM
var holder = this.treeNodeHolder();
if(before) holder.insertBefore(droppable, before);
else holder.appendChild(droppable);
// Make droppable
var customOptions = holder.parentNode.dropperOptions ? Object.extend(holder.parentNode.dropperOptions, TreeNodeSeparatorDropper) : TreeNodeSeparatorDropper;
if(!customOptions.accept != 'none') {
if(Droppables) Droppables.add(droppable, customOptions);
}
}
}
TreeNodeDragger = {
onStartDrag : function(dragger) {
dragger.oldParent = dragger.parentTreeNode;
},
revert: true
}
TreeNodeDropper = {
onDrop : function(dragger, dropper, event) {
var result = true;
// Handle event handlers
if(dragger.onParentChanged && dragger.parentTreeNode != dropper.treeNode)
result = dragger.onParentChanged(dragger, dragger.parentTreeNode, dropper.treeNode);
// Get the future order of the children after the drop completes
var i = 0, item = null, items = [];
items[items.length] = dragger.treeNode;
for(i=0;item=dropper.treeNode.treeNodeHolder().childNodes[i];i++) {
if(item != dragger.treeNode) items[items.length] = item;
}
if(result && dragger.onOrderChanged)
result = dragger.onOrderChanged(items, items[0]);
if(result) {
dropper.treeNode.appendTreeNode(dragger.treeNode, dropper.treeNode.firstTreeNode());
}
dragger.wasDragged = true;
},
hoverclass : 'dragOver',
checkDroppableIsntContained : true
}
TreeNodeSeparatorDropper = {
onDrop : function(dragger, dropper, event) {
var result = true;
// Handle parent-change handlers
if(dragger.onParentChanged && dragger.parentTreeNode != dropper.treeNode)
result = dragger.onParentChanged(dragger, dragger.parentTreeNode, dropper.treeNode);
// Get the future order of the children after the drop completes
var i = 0, item = null, items = [];
for(i=0;item=dropper.treeNode.treeNodeHolder().childNodes[i];i++) {
if(item == dropper) items[items.length] = dragger.treeNode;
if(item != dragger.treeNode) items[items.length] = item;
}
// Handle order change
if(result && dragger.onOrderChanged)
result = dragger.onOrderChanged(items, dragger.treeNode);
if(result) {
dropper.treeNode.appendTreeNode(
dragger.treeNode, dropper);
}
dragger.wasDragged = true;
},
hoverclass : 'dragOver',
greedy : true,
checkDroppableIsntContained : true
}
//---------------------------------------------------------------------------------------------///
/**
* Mix-in for the tree to enable mulitselect support
* Usage:
* - tree.behaveAs(MultiselectTree)
* - tree.stopBehavingAs(MultiselectTree)
*/
MultiselectTree = Class.create();
MultiselectTree.prototype = {
initialize: function() {
Element.addClassName(this, 'multiselect');
this.MultiselectTree_observer = this.observeMethod('NodeClicked', this.multiselect_onClick.bind(this));
this.selectedNodes = { }
},
destroyDraggable: function() {
this.stopObserving(this.MultiselectTree_observer);
},
multiselect_onClick : function(selectedNode) {
if(selectedNode.selected) {
this.deselectNode(selectedNode);
} else {
this.selectNode(selectedNode);
}
// Trigger the onselect event
return true;
},
selectNode: function(selectedNode) {
var idx = this.getIdxOf(selectedNode);
selectedNode.addNodeClass('selected');
selectedNode.selected = true;
this.selectedNodes[idx] = selectedNode.aTag.innerHTML;
},
deselectNode : function(selectedNode) {
var idx = this.getIdxOf(selectedNode);
selectedNode.removeNodeClass('selected');
selectedNode.selected = false;
delete this.selectedNodes[idx];
}
}