/**
* The fastwiki plugin loads 'do' actions as AJAX requests when possible, to speed up the page. It also adds section editing.
*/
var plugin_fastwiki = (function($) {
var m_viewMode, m_origViewMode, m_prevView; // show, edit, subscribe etc
var m_isSecedit, m_wasSecedit;
var m_hasDraft;
var m_pageObjs = {}; // Edit objects
var m_content;
var m_initialId;
var m_debug = document.location.host == 'localhost';
var m_cache = new CPageCache(JSINFO.fastwiki.preload_per_page, JSINFO.fastwiki.preload_batchsize, m_debug);
// Unsupported actions, and reason for lack of support:
// login, register and resendpwd: Templates, plugins or future versions of dokuwiki might make them https.
// admin: Admin can change things outside the main content area.
// conflict, denied and locked: I don't know what they do.
// showtag: It doesn't work properly with unicode tags.
var m_supportedActions = {'':1, edit:1, draft:1, history:1, recent:1, revisions:1, show:1, subscribe:1, backlink:1, index:1, profile:1, media:1, diff:1, save:1};
var m_modeClassElt;
var m_browserHistory = new CBrowserHistory();
//////////
// On load initialization
//////////
$(function() {
// Leaving imgdetail with ajax is just too complicated to support.
if (document.location.href.indexOf("detail.php") >= 0)
m_viewMode = 'unsupported';
else {
var urlParams = _urlToObj(document.location.href);
m_viewMode = urlParams['do'] || 'show';
if (!m_supportedActions[m_viewMode])
m_viewMode = 'unsupported';
else if (window.tpl_fastwiki_startmode_support && !(m_viewMode in tpl_fastwiki_startmode_support))
m_viewMode = 'unsupported';
}
m_origViewMode = m_viewMode;
// plugin_fastwiki_marker was added by the action plugin. It makes it possible to find the main content area regardless of the template used.
m_content = $('.plugin_fastwiki_marker').parent();
m_content.addClass('content_initial');
m_initialId = m_content.attr('id');
m_modeClassElt = m_content.hasClass('dokuwiki') ? m_content : $(m_content.parents('.dokuwiki')[0] || document.body);
$(window).trigger('fastwiki:init', [m_viewMode]);
if (JSINFO.fastwiki.fastpages)
fixActionLinks(document.body);
// The feature is not supported by IE 9 and below.
if (JSINFO.fastwiki.fastshow && (m_origViewMode != 'show' || !window.history || !history.pushState))
JSINFO.fastwiki.fastshow = false;
if (JSINFO.fastwiki.fastshow)
m_browserHistory.init(load);
});
/**
* Find supported action links (do=???) and references to the current page, and turn them into AJAX requests.
*
* @param {DOMNode} elt - Do it inside this element.
*/
function fixActionLinks(elt) {
if (m_origViewMode == 'unsupported')
return;
var formActions = {search: 1};
var supportedFields = {'do':1, rev:1, id:1};
// TODO: Support search: Hook search box, not just href. Note that supporting search changes doku behavior -- search results now have namespaces and origin pages.
// Because of this, search will have to be a separate config setting.
// TODO: Profile needs button hooks.
// Intercept all action (do=) urls, switching them to AJAX.
$('a[href *= "?do="]', elt).click(function(e) {
if (this.href.indexOf('block_fastwiki') < 0) {
var curPathId = JSINFO.id.replace(/:/g, '/');
var aPathId = this.href.replace(/\?.*$/, '').replace(/:/g, '/');
// The magic only works if the do= is for the current page.
if (aPathId.substr(-curPathId.length) == curPathId) {
var params = _urlToObj(this.href);
if (!params['do'])
params['do'] = 'show';
if (params['do'] in m_supportedActions) {
e.preventDefault();
load(params['do'], null, params);
}
}
}
});
$('input[type="submit"], input[type="button"], button', elt).click(function(e) {
var form = $(this).parents('form');
if (form.length > 0 && form[0]['do'] && form[0]['do'].value in m_supportedActions) {
// For now, only allow very simple forms
var supported = true;
$('input[type != "submit"]', form).each(function(idx, elt) {
if (elt.type != 'button' && (elt.type != 'hidden' || !(elt.name in supportedFields)))
supported = false;
});
if (supported) {
e.preventDefault();
var params = _formToObj(form[0]);
if (!params['do'])
params['do'] = 'show';
load(params['do'], null, params);
}
}
});
// Only fix self-referrential links if we started out in show mode.
if (m_origViewMode == 'show' && window.JSINFO) {
var pathId = JSINFO.id.replace(/:/g, '/');
// Handle all anchors instead of using CSS selectors to narrow it down, since the current id can change.
$('a', elt).click(function(e) {
// TODO Document: Doesn't work with cannonical url feature.
var href = this.getAttribute('href'); // Use getAttribute because some browsers make href appear to be cannonical.
if (href && href.indexOf('://') < 0) {
if (href.match(m_browserHistory.getSelfRefRegex())) {
load('show');
e.preventDefault();
}
else if (JSINFO.fastwiki.fastshow) {
var numParams = href.split('=').length;
if (href.indexOf('id=') >= 0)
numParams--;
if (numParams == 1) {
//TODO: What about pages that aren't in the wiki at all? Forums etc. Use a config field?
if (m_browserHistory.switchBasePath(href)) {
m_viewMode = null;
e.preventDefault();
}
}
}
}
});
// Old selector:
// 'a[href $= "doku.php?id=' + JSINFO.id + '"], a[href $= "doku.php/' + pathId + '"], a[href = "/' + pathId + '"]'
}
// Inline section edit
if (JSINFO.fastwiki.secedit) {
$('.editbutton_section .btn_secedit input[type=submit], .editbutton_section .btn_secedit button', elt).click(function(e) {
e.preventDefault();
var form = $(this).parents('form');
load('edit', form, _formToObj(form));
});
}
if (JSINFO.fastwiki.preload)
m_cache.load(elt, m_browserHistory);
}
/**
* Preview a page edit without reloading the page.
*
* @private
* @param {Form=} sectionForm - If a section is being edited instead of the whole document, this is the form in that section.
*/
function _preview(sectionForm) {
var body = $(document.body);
var params = _formToObj($('#dw__editform'));
params['do'] = 'preview';
_sendPartial(params, $('.dokuwiki .editBox'), function(data) {
var preview = $('
' + data + '
');
// In case anything changed, migrate values over to the existing form.
var pvEditor = preview.find('#dw__editform');
var editor = $('#dw__editform')[0];
pvEditor.find('input[type=hidden]').each(function(idx, elt) {
editor[elt.name].value = elt.value;
});
// Strip out the editor. We already have that.
preview.find('#scroll__here').prevAll().remove();
var oldpreview = $('#preview_container');
if (oldpreview.length > 0)
oldpreview.replaceWith(preview);
else
$('.content_partial').append(preview);
setTimeout(function() {
$('html,body').animate({scrollTop: $('#scroll__here').offset().top+'px'}, 300);
}, 1);
}, 'text');
}
/**
* Get an editable page section.
* Algorithm taken from dw_page.sectionHighlight().
*
* @private
* @param {Form=} sectionForm - The form representing the editable section.
*/
function _getSection(sectionForm) {
var pieces = $();
var target = sectionForm.parent();
var nr = target.attr('class').match(/(\s+|^)editbutton_(\d+)(\s+|$)/)[2];
// Walk the dom tree in reverse to find the sibling which is or contains the section edit marker
while (target.length > 0 && !(target.hasClass('sectionedit' + nr) || target.find('.sectionedit' + nr).length)) {
target = target.prev();
// section_highlight is a temporary div that wraps around the text when your cursor is
// over the edit icon. Since this div will go away when edit starts, you don't want to
// keep the div itself. Instead, grab all of its children.
if (target.hasClass('section_highlight'))
pieces = pieces.add(target.children());
else
pieces = pieces.add(target);
}
return pieces;
}
/**
* Switch focus to the editor.
*/
function _focusEdit() {
var $edit_text = $('#wiki__text');
if ($edit_text.length > 0 && !$edit_text.attr('readOnly')) {
// set focus and place cursor at the start
var sel = DWgetSelection($edit_text[0]);
sel.start = 0;
sel.end = 0;
try {
DWsetSelection(sel);
}
catch(e) {
// DWsetSelection can sometimes fail in FireFox. Try again later.
setTimeout(function() {
try {
DWsetSelection(sel);
}
catch(e) {
}
}, 500);
}
$edit_text.focus();
}
}
/**
* Initialize a page edit. This must be called every time the editor is loaded.
* Most of this function was derived from core DokuWiki scripts, because Doku doesn't have init functions -- it does
* all initialization in global jQuery DOMContentReady scope.
*
* @private
*/
function _initEdit() {
// Comatibility: before 2016-06-26a
if (!window.doku_summaryCheck) {
window.doku_summaryCheck = window.summaryCheck;
}
dw_editor.init();
dw_locktimer.init(JSINFO.fastwiki.locktime, JSINFO.fastwiki.usedraft);
// From edit.js
var $editform = $('#dw__editform');
if ($editform.length == 0)
return;
var $edit_text = $('#wiki__text');
if ($edit_text.length > 0) {
if($edit_text.attr('readOnly')) {
return;
}
// set focus and place cursor at the start
var sel = DWgetSelection($edit_text[0]);
sel.start = 0;
sel.end = 0;
DWsetSelection(sel);
$edit_text.trigger('focus');
doku_edit_text_content = $edit_text.val();
}
$editform.on("change keydown", function(e) {
window.textChanged = true;
doku_summaryCheck();
});
m_pageObjs.content = $edit_text.val();
window.onbeforeunload = function() {
if (window.textChanged && m_pageObjs.content != $edit_text.val())
return LANG.notsavedyet;
};
window.onunload = deleteDraft;
$('#edbtn__preview').click(function(e) {
if (JSINFO.fastwiki.preview) {
e.preventDefault();
_preview(m_pageObjs.sectionForm);
m_hasDraft = true;
dw_locktimer.reset();
}
else {
// Original behavior from edit.js.
window.onbeforeunload = '';
textChanged = false;
window.keepDraft = true;
}
});
$('#edit__summary').on("change keyup", doku_summaryCheck);
if (textChanged)
doku_summaryCheck();
// From toolbar.js
initToolbar('tool__bar','wiki__text', toolbar);
$('#tool__bar').attr('role', 'toolbar');
// Work-around for https://github.com/splitbrain/dokuwiki/issues/3466
// See lib/scripts/linkwiz.js:init
setTimeout(function() {
dw_linkwiz.$wiz = null;
$('#link__wiz').remove();
// Position is usually calculated from the editor, which breaks if the editor has a positioned parent.
// Instead, calculate based on the button.
dw_linkwiz.init($('[aria-controls="link__wiz"]'));
// Now restore linkwiz's link to the editor.
dw_linkwiz.textArea = $('#wiki__text')[0];
}, 0);
// reset change memory var on submit
$('#edbtn__save').click(function(e) {
textChanged = false;
// Do a fast save if we started on 'show' and we're not creating or deleting the page.
if (JSINFO.fastwiki.save && m_origViewMode == 'show' && $edit_text.val().length>0 && m_pageObjs.content.length>0) {
e.preventDefault();
load('save', null, _formToObj($('#dw__editform')));
}
// Invalidate the cache if fastwiki.save is off. If it's on, the cache will be updated after save.
else
m_cache.remove(JSINFO.id);
window.onbeforeunload = '';
dw_locktimer.clear();
});
// Cancel button on edit, or Delete Draft button on draft.
$('input[name="do[draftdel]"]', $editform).click(function(e) {
e.preventDefault();
var id = $editform.find('input[name=id]').val();
load('show');
if (!window.keepDraft) {
// Silently remove a possibly saved draft using ajax.
jQuery.post(DOKU_BASE + 'lib/exe/ajax.php', {
call: 'draftdel',
id: id,
success: function(data, textStatus, jqXHR) {
m_hasDraft = false;
}
});
}
});
// Cancel button on draft
$('input[name="do[show]"]', $editform).click(function(e) {
e.preventDefault();
load('show');
});
$('.picker.pk_hl').addClass('dokuwiki');
}
/**
* Change the current body class to represent the given action.
*
* @private
* @param {String} action - The new page action.
* @param {String=} target - The part of the page being targetted. Can be one of: {section}
*/
function _setBodyClass(action, target) {
for (var k in m_supportedActions)
m_modeClassElt.removeClass('mode_' + k);
m_modeClassElt.addClass('mode_'+action);
// Special case for section edit.
if (target == 'section')
m_modeClassElt.removeClass('mode_edit').addClass('mode_show mode_secedit');
$('.content_partial').toggle(m_viewMode != m_origViewMode);
$('.content_initial').toggle(m_viewMode == m_origViewMode || target == 'section');
}
/**
* Update page objects on view switch.
*
* @private
*/
function _updatePageObjsOnSwitch() {
if (m_pageObjs.showOnSwitch)
m_pageObjs.showOnSwitch.show();
delete m_pageObjs.showOnSwitch;
delete m_pageObjs.content;
delete m_pageObjs.sectionForm;
var hasToc = {show: 1, admin: 1};
// #dw__toc is common to all templates. #dw_toc_head is from the zioth template. #dw_toc is from starterbootstrap
$("#dw__toc, #dw_toc_head, #dw_toc").css('display', m_viewMode in hasToc ? '' : 'none');
}
/**
* Convert a form to an object suitable for $.post().
*
* @private
*/
function _formToObj(form) {
var obj = {};
$(form).serializeArray().map(function(item){obj[item.name] = item.value;});
return obj;
}
/**
* Convert a url to an object suitable for $.post().
*
* @private
*/
function _urlToObj(url) {
var obj = {};
var a = url.replace(/.*\?/, '').split('&');
for (var x=0; x').append(data);
$('.content_initial').before(body);
}, 'text');
}
var form = $('#subscribe__form');
$('input[name="do[subscribe]"]', form).click(function(e) {
e.preventDefault();
subscribeAction(_formToObj(form));
});
$('.content_partial .unsubscribe').click(function(e) {
e.preventDefault();
subscribeAction(_urlToObj(this.href));
});
},
index: function(params, extraData) {
// Global init from index.js
dw_index.$obj = $('#index__tree');
dw_index.init();
},
edit: function(params, extraData) {
var draft = params['do'] == 'draft';
if (m_hasDraft === true)
draft = true;
else if (m_hasDraft === false)
draft = params.rev = null;
if (extraData.sectionForm) {
// Define showOnSwitch here, not above, so _updatePageObjsOnSwitch doesn't re-show them too early.
m_pageObjs.sectionForm = extraData.sectionForm; // Redefine.
extraData.sectionParts = extraData.sectionParts.add('.editbutton_section');
m_pageObjs.showOnSwitch = extraData.sectionParts;
m_pageObjs.showOnSwitch.hide();
_initEdit();
_focusEdit();
}
else
_initEdit();
setTimeout(function() {
// Focusing the editor causes the browser to scroll, so wait until it's likely to be in view (after the page is rearranged) before calling this.
_focusEdit();
if (document.body.scrollTop > 0)
$('html,body').animate({scrollTop: Math.max(0, Math.floor($('.content_partial').offset().top)-20)+'px'}, 300);
}, 1);
},
revisions: function(params, extraData) {
$('.content_partial form').each(function(idx, form) {
$('input[name="do[diff]"]', form).click(function(e) {
e.preventDefault();
load('diff', null, _formToObj(form));
});
});
},
save: function(params, extraData) {
// If dates don't match, there's a conflict.
if ($('.content_partial #a_newer_version_exists').length > 0) {
m_viewMode = 'edit';
m_actionEffects.edit(params, m_pageObjs.sectionForm ? {sectionForm: m_pageObjs.sectionForm, sectionParts:_getSection(m_pageObjs.sectionForm)} : {});
var editform = $('#dw__editform');
$('input[name="do[save]"]', editform).click(function(e) {
e.preventDefault();
load('save', null, _formToObj(editform));
window.onbeforeunload = '';
dw_locktimer.clear();
});
// Cancel button on edit, or Delete Draft button on draft.
$('input[name="do[cancel]"]', editform).click(function(e) {
e.preventDefault();
var id = editform.find('input[name=id]').val();
load('show');
if (!window.keepDraft) {
// Silently remove a possibly saved draft using ajax.
jQuery.post(DOKU_BASE + 'lib/exe/ajax.php', {
call: 'draftdel',
id: id,
success: function(data, textStatus, jqXHR) {
m_hasDraft = false;
}
});
}
});
}
// Recoverable error. Return to the edit form.
else if ($('.content_partial #dw__editform').length > 0) {
m_viewMode = 'edit';
m_actionEffects.edit(params, m_pageObjs.sectionForm ? {sectionForm: m_pageObjs.sectionForm, sectionParts:_getSection(m_pageObjs.sectionForm)} : {});
}
// When ACL fails, a preview is returned.
else if ($('.content_partial #preview').length > 0) {
//TODO: How do I handle this case? Test.
load('show');
}
else {
$('.content_initial').html($('.content_partial').html());
$('.content_partial').remove();
// The html() transfer above lost dynamic events. Reset.
fixActionLinks($('.content_initial'));
m_browserHistory.refreshPageTitle(false);
load('show');
// These two lines are from dw_page.init()
dw_page.sectionHighlight();
jQuery('a.fn_top').mouseover(dw_page.footnoteDisplay);
}
},
show: function(params, extraData) {
$('.content_initial').html($('.content_partial').html());
$('.content_partial').remove();
// The html() transfer above lost dynamic events. Reset.
fixActionLinks($('.content_initial'));
// These two lines are from dw_page.init()
dw_page.sectionHighlight();
jQuery('a.fn_top').mouseover(dw_page.footnoteDisplay);
}
};
m_actionEffects.draft = m_actionEffects.edit;
m_actionEffects.diff = m_actionEffects.revisions;
/**
* Perform a standard partial AJAX action (edit, history, etc).
*
* @param {DOMNode=} insertLoc - Optional
* @private
*/
function _action(action, params, callback, insertLoc, extraData) {
params['do'] = action;
function cb(data) {
$('.content_partial, .message_partial').remove();
$('.content_initial').attr('id', m_initialId);
var tagname = $('.content_initial')[0].tagName.toLowerCase();
var body = $('<' + tagname + ' class="content_partial">' + tagname + '>').append(data);
//TODO: I don't like having to put special case code here. Is there any better place to put it? m_actionEffects is too late.
if (insertLoc && action=='edit') {
var newform = $('#dw__editform', body);
if (newform.find('input[name=prefix]').val() == '.' && newform.find('input[name=suffix]').val() == '') {
// There was an error and the whole page is being edited, or there was only one section on the page.
delete m_pageObjs.sectionForm;
if (extraData)
delete extraData.sectionForm;
insertLoc = null;
}
}
if (insertLoc)
$(insertLoc[insertLoc.length - 1]).after(body);
// This kind of partial replaces the whole content area.
else {
// Swap ids and classes, so the new element is styled correctly.
var initial = $('.content_initial');
body.addClass(initial[0].className.replace(/content_initial/, '')).attr('id', m_initialId);
initial.attr('id', '').after(body);
}
// fwJustincase is to catch the case where the callbacks modify the DOM enough that this element not longer exists.
var newToc = $('.content_partial #dw__toc').addClass('fwJustincase');
var hasNewToc = newToc.length > 0;
_updatePageObjsOnSwitch();
if (callback)
callback(data, extraData);
if (m_actionEffects[action])
m_actionEffects[action](params, extraData||{});
// Update TOC. The default behavior is just to leave it in place, where it comes in.
newToc = $('.fwJustincase').removeClass('fwJustincase');
if (m_viewMode == 'show')
$(window).trigger('fastwiki:updateToc', [newToc]);
// Initialize TOC. Must happen after m_actionEffects, which can overwrite the HTML and lose events.
if (hasNewToc)
dw_page.makeToggle('#dw__toc h3','#dw__toc > div');
// Update links in the content area.
fixActionLinks($('.content_partial'));
// It's important to use m_viewMode here instead of action, because the callbacks can change the action.
_setBodyClass(m_viewMode, m_pageObjs.sectionForm ? "section" : null);
// Cache the page.
if (m_viewMode == 'show')
m_cache.add(JSINFO.id, data, true);
// Update doku state.
if (!insertLoc)
dw_behaviour.init();
$(window).trigger('fastwiki:afterSwitch', [m_pageObjs.sectionForm?'show':m_viewMode, !!m_pageObjs.sectionForm, m_prevView]);
if (!m_isSecedit && !m_wasSecedit) {
setTimeout(function() {
$('html,body').animate({scrollTop: 0}, 300);
}, 1);
}
}
//TODO: On save, refresh cache.
// If the page is cached, load it from cache.
if (action == 'show' && m_cache.get(JSINFO.id)) {
m_debug && console.log("Getting from cache: " + JSINFO.id);
cb(m_cache.get(JSINFO.id));
}
else
_sendPartial(params, _getVisibleContent(), cb, 'text');
}
/**
* Send a "partial" action, used for AJAX editing, previews, subscribe etc.
*
* @param {Object} params - Parameters to send to doku.php.
* @param {DOMNode} spinnerParent - Center the loading spinner in this object.
* @param {Function} callback - Call this function, with the content HTML as a parameter, when the action is complete.
* @private
*/
function _sendPartial(params, spinnerParent, callback) {
if ($('.partialsLoading').length == 0) {
var spinnerCss = spinnerParent.height() + spinnerParent.offset().top > $(window).height() ? {top: $(window).height() / 2} : {top: '50%'};
spinnerParent.append($('').css('display', 'none').css(spinnerCss));
// Give it some time in case the server is really responsive.
setTimeout(function() {$('.partialsLoading').css('display', '');}, 500);
}
params.partial = 1;
jQuery[!params['do'] || params['do']=='show' ? 'get' : 'post'](m_browserHistory.getBaseUrl(), params, function(data) {
// Special error conditions
if (data == 'PERMISSION_CHANGE') {
delete params.partial;
delete params.fastwiki_compareid;
var url = m_browserHistory.getBaseUrl() + '?' + $.param(params);
document.location.href = url;
}
else
callback(data);
// Remove all loading spinners, in case a bug let some extras slip in.
$('.partialsLoading').remove();
}, 'text');
}
/**
* Return the currently visible content area.
*/
function _getVisibleContent() {
var parentElt = $('.content_partial');
if (parentElt.length == 0)
parentElt = $('.content_initial');
return parentElt;
}
/**
* Load a new view, using AJAX to avoid page re-load.
*
* @param {String} page - The view to load. This can be 'show,' or the value of a do= action param.
* @param {Form=} sectionForm - Only valid when page=='edit' or page=='draft'. Used to edit a section inline.
* @param {Object=} params - Additional parameters to pass to the AJAX request. For example, 'rev' if a revision is being edited.
* @param {boolean=} force - Force an AJAX load, even if the code thinks it can optimize it out.
* @param {Function=} callback - Called after the new page is loaded.
*/
function load(page, sectionForm, params, force, callback) {
//TODO: What if load() is called while another load() is active?
// If edit text has changed, confirm before switching views.
if ((m_viewMode == 'edit' || m_viewMode == 'draft') && (page != 'save' && page != 'preview') && m_pageObjs.content != $('#wiki__text').val()) {
if (!confirm(LANG.notsavedyet))
return;
}
m_prevView = m_viewMode;
// Edit: was=false, is=true
// Save: was=true, is=true
// Show: was=true, is=false
m_wasSecedit = m_isSecedit;
m_isSecedit = !!sectionForm || (m_wasSecedit && page=='save');
m_viewMode = page;
if (!params)
params = {};
window.onbeforeunload = '';
dw_locktimer.clear();
// First switch back to the original mode, canceling other modes.
_updatePageObjsOnSwitch();
// If we're back to the original mode, just clean up and quit.
if (page == m_origViewMode && !force) {
$('.content_partial, .message_partial').remove();
$('.content_initial').attr('id', m_initialId);
if (m_prevView != page) {
// Scroll to top.
if (!m_isSecedit && !m_wasSecedit) {
setTimeout(function() {
$('html,body').animate({scrollTop: 0}, 300);
}, 1);
}
_setBodyClass(page);
$(window).trigger('fastwiki:afterSwitch', [m_pageObjs.sectionForm?'show':m_viewMode, !!m_pageObjs.sectionForm]);
}
if (callback)
callback();
}
// Sectionedit is special. Other special handlers are in m_actionEffects.
else if ((page == 'draft' || page == 'edit') && sectionForm) {
var sectionParts = _getSection(sectionForm);
_action(page, params, callback, sectionParts, {sectionForm: sectionForm, sectionParts:sectionParts});
}
// Default action
else
_action(page, params, callback);
}
return {
load: load,
fixActionLinks: fixActionLinks
};
/* DOKUWIKI:include pagecache.js */
/* DOKUWIKI:include history.js */
})(jQuery);
/* DOKUWIKI:include templates.js */
/* DOKUWIKI:include plugins.js */