/**
* Basic sample plugin inserting footnotes elements into CKEditor editing area.
*
* Version 1.0.9
* https://github.com/andykirk/CKEditorFootnotes
*
*/
// Register the plugin within the editor.
CKEDITOR.plugins.add( 'footnotes', {
footnote_ids: [],
requires: 'widget',
icons: 'footnotes',
// The plugin initialization logic goes inside this method.
init: function(editor) {
// Allow `cite` to be editable:
CKEDITOR.dtd.$editable['cite'] = 1;
// Add some CSS tweaks:
var css = '.footnotes{background:#eee; padding:1px 15px;} .footnotes cite{font-style: normal;}';
CKEDITOR.addCss(css);
var $this = this;
editor.on('saveSnapshot', function(evt) {
console.log('saveSnapshot');
});
// Force a reorder on startup to make sure all vars are set: (e.g. footnotes store):
editor.on('instanceReady', function(evt) {
$this.reorderMarkers(editor);
});
// Add the reorder change event:
editor.on('change', function(evt) {
// Copy the footnotes_store as we may be doing a cut:
if(!evt.editor.footnotes_tmp) {
evt.editor.footnotes_tmp = evt.editor.footnotes_store;
}
// Prevent no selection errors:
if (!evt.editor.getSelection().getStartElement()) {
return;
}
// Don't reorder the markers if editing a cite:
var footnote_section = evt.editor.getSelection().getStartElement().getAscendant('section');
if (footnote_section && footnote_section.$.className.indexOf('footnotes') != -1) {
return;
}
// SetTimeout seems to be necessary (it's used in the core but can't be 100% sure why)
setTimeout(function(){
$this.reorderMarkers(editor);
},
0
);
});
// Build the initial footnotes widget editables definition:
var prefix = editor.config.footnotesPrefix ? '-' + editor.config.footnotesPrefix : '';
var def = {
header: {
selector: 'header > *',
//allowedContent: ''
allowedContent: 'strong em span sub sup;'
}
};
var contents = jQuery('
' + editor.element.$.textContent + '
')
, l = contents.find('.footnotes li').length
, i = 1;
for (i; i <= l; i++) {
def['footnote_' + i] = {selector: '#footnote' + prefix + '-' + i + ' cite', allowedContent: 'a[href]; cite[*](*); strong em span br'};
}
// Register the footnotes widget.
editor.widgets.add('footnotes', {
// Minimum HTML which is required by this widget to work.
requiredContent: 'section(footnotes)',
// Check the elements that need to be converted to widgets.
upcast: function(element) {
return element.name == 'section' && element.hasClass('footnotes');
},
editables: def
});
// Register the footnotemarker widget.
editor.widgets.add('footnotemarker', {
// Minimum HTML which is required by this widget to work.
requiredContent: 'sup[data-footnote-id]',
// Check the elements that need to be converted to widgets.
upcast: function(element) {
return element.name == 'sup' && element.attributes['data-footnote-id'] != 'undefined';
}
});
// Define an editor command that opens our dialog.
editor.addCommand('footnotes', new CKEDITOR.dialogCommand('footnotesDialog', {
// @TODO: This needs work:
allowedContent: 'section[*](*);header[*](*);li[*];a[*];cite(*)[*];sup[*]',
requiredContent: 'section[*](*);header[*](*);li[*];a[*];cite(*)[*];sup[*]'
}));
// Create a toolbar button that executes the above command.
editor.ui.addButton('Footnotes', {
// The text part of the button (if available) and tooptip.
label: 'Insert Footnotes',
// The command to execute on click.
command: 'footnotes',
// The button placement in the toolbar (toolbar group name).
toolbar: 'insert'
});
// Register our dialog file. this.path is the plugin folder path.
CKEDITOR.dialog.add('footnotesDialog', this.path + 'dialogs/footnotes.js');
},
editorContents: function(editor) {
if (editor.config.footnotesEditorSelector) {
return jQuery(eval(editor.config.footnotesEditorSelector)).contents()
} else {
return jQuery('#' + editor.id + '_contents iframe').contents().find('body');
}
},
build: function(footnote, is_new, editor) {
if (is_new) {
// Generate new id:
footnote_id = this.generateFootnoteId();
} else {
// Existing footnote id passed:
footnote_id = footnote;
}
// Insert the marker:
var footnote_marker = 'X';
editor.insertHtml(footnote_marker);
if (is_new) {
editor.fire('lockSnapshot');
this.addFootnote(this.buildFootnote(footnote_id, footnote, false, editor), editor);
editor.fire('unlockSnapshot');
}
this.reorderMarkers(editor);
},
buildFootnote: function(footnote_id, footnote_text, data, editor) {
data ? data : false;
var links = '';
var letters = 'abcdefghijklmnopqrstuvwxyz';
var order = data ? data.order.indexOf(footnote_id) + 1
: 1;
var prefix = editor.config.footnotesPrefix ? '-' + editor.config.footnotesPrefix : '';
if (data && data.occurrences[footnote_id] == 1) {
links = '^ ';
} else if (data && data.occurrences[footnote_id] > 1) {
var i = 0
, l = data.occurrences[footnote_id]
, n = l;
for (i; i < l; i++) {
links += '' + letters.charAt(i) + '';
if (i < l-1) {
links += ', ';
} else {
links += ' ';
}
}
}
footnote = '';
return footnote;
},
addFootnote: function(footnote, editor) {
$contents = this.editorContents(editor);
$footnotes = $contents.find('.footnotes');
if ($footnotes.length == 0) {
var container = '';
// Move cursor to end of content:
var range = editor.createRange();
range.moveToElementEditEnd(range.root);
editor.getSelection().selectRanges([range]);
// Insert the container:
editor.insertHtml(container);
} else {
$footnotes.find('ol').append(footnote);
}
return;
},
generateFootnoteId: function() {
var id = Math.random().toString(36).substr(2, 5);
while (jQuery.inArray(id, this.footnote_ids) != -1) {
id = String(this.generateFootnoteId());
}
this.footnote_ids.push(id);
return id;
},
reorderMarkers: function(editor) {
editor.fire('lockSnapshot');
var prefix = editor.config.footnotesPrefix ? '-' + editor.config.footnotesPrefix : '';
$contents = this.editorContents(editor);
var data = {
order: [],
occurrences: {}
};
// Check that there's a footnotes section. If it's been deleted the markers are useless:
if ($contents.find('.footnotes').length == 0) {
$contents.find('sup[data-footnote-id]').remove();
editor.fire('unlockSnapshot');
return;
}
// Find all the markers in the document:
var $markers = $contents.find('sup[data-footnote-id]');
// If there aren't any, remove the Footnotes container:
if ($markers.length == 0) {
$contents.find('.footnotes').parent().remove();
editor.fire('unlockSnapshot');
return;
}
// Otherwise reorder the markers:
$markers.each(function(){
var footnote_id = jQuery(this).attr('data-footnote-id')
, marker_ref
, n = data.order.indexOf(footnote_id);
// If this is the markers first occurrence:
if (n == -1) {
// Store the id:
data.order.push(footnote_id);
n = data.order.length;
data.occurrences[footnote_id] = 1;
marker_ref = n + '-1';
} else {
// Otherwise increment the number of occurrences:
// (increment n due to zero-index array)
n++;
data.occurrences[footnote_id]++;
marker_ref = n + '-' + data.occurrences[footnote_id];
}
// Replace the marker contents:
var marker = '';
jQuery(this).html(marker);
});
// Prepare the footnotes_store object:
editor.footnotes_store = {};
// Then rebuild the Footnotes content to match marker order:
var footnotes = '';
var footnote_text = '';
var i = 0
, l = data.order.length;
for (i; i < l; i++) {
footnote_id = data.order[i];
footnote_text = $contents.find('.footnotes [data-footnote-id=' + footnote_id + '] cite').html();
// If the footnotes text can't be found in the editor, it may be in the tmp store
// following a cut:
if (!footnote_text) {
footnote_text = editor.footnotes_tmp[footnote_id];
}
footnotes += this.buildFootnote(footnote_id, footnote_text, data, editor);
// Store the footnotes for later use (post cut/paste):
editor.footnotes_store[footnote_id] = footnote_text;
}
// Insert the footnotes into the list:
$contents.find('.footnotes ol').html(footnotes);
// Next we need to reinstate the 'editable' properties of the footnotes.
// (we have to do this individually due to Widgets 'fireOnce' for editable selectors)
var el = $contents.find('.footnotes')
, footnote_widget;
// So first we need to find the right Widget instance:
// (I hope there's a better way of doing this but I can't find one)
for (i in editor.widgets.instances) {
if (editor.widgets.instances[i].name == 'footnotes') {
footnote_widget = editor.widgets.instances[i];
break;
}
}
// Then we `initEditable` each footnote, giving it a unique selector:
for (i in data.order) {
n = parseInt(i) + 1;
footnote_widget.initEditable('footnote_' + n, {selector: '#footnote' + prefix + '-' + n +' cite', allowedContent: 'a[href]; cite[*](*); em strong span'});
}
editor.fire('unlockSnapshot');
return;
}
});