WP Plugin Starter 1.0.0: a fresh start
The parts every wpallstars plugin needs: settings screen with search and an empty General tab, Read Me tab, features as classes, replaced plugins, the shared GitHub updater, release and check scripts, CI and the shared rules. scripts/rename-plugin.sh starts a new plugin; scripts/sync-core.sh keeps plugins' core files in step with this starter. Built with aidevops (https://aidevops.sh).
This commit is contained in:
commit
2f91c3e6af
62 files changed
+10929
No files matched your search
@@ -0,0 +1,322 @@
|
||||
/**
|
||||
* WP Plugin Starter admin screen.
|
||||
*
|
||||
* One delegated controller per concern:
|
||||
* - Settings: instant save for [data-wps-setting] controls.
|
||||
* - Panels: expandable setting options.
|
||||
* - Tokens: insert pattern tokens into text fields.
|
||||
* - Media fields: choose a Media Library picture with the media dialog.
|
||||
*
|
||||
* The plugin's own tabs bring their own script (wpstarter_admin_enqueue),
|
||||
* which can use window.wpstarterAdmin.api.
|
||||
*
|
||||
* Localized data: window.wpstarterAdmin (see WPStarter_Admin_Manager::enqueue_assets()).
|
||||
*/
|
||||
(function ($, wp, cfg) {
|
||||
'use strict';
|
||||
|
||||
if (!cfg) {
|
||||
return;
|
||||
}
|
||||
|
||||
var i18n = cfg.i18n || {};
|
||||
|
||||
function speak(message, politeness) {
|
||||
if (wp && wp.a11y && wp.a11y.speak) {
|
||||
wp.a11y.speak(message, politeness || 'polite');
|
||||
}
|
||||
}
|
||||
|
||||
// Some plugins redirect the first admin request after activation to a
|
||||
// welcome screen, and admin-ajax.php runs admin_init too. The redirect
|
||||
// happens once, so a reply that is a page instead of JSON is retried
|
||||
// once. Every action here is safe to repeat.
|
||||
function post(action, data) {
|
||||
var deferred = $.Deferred();
|
||||
var current;
|
||||
var send = function (retry) {
|
||||
current = $.ajax({
|
||||
url: cfg.ajaxUrl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: $.extend({ action: action, nonce: cfg.nonce }, data)
|
||||
})
|
||||
.done(deferred.resolve)
|
||||
.fail(function (xhr, status, error) {
|
||||
if (status === 'parsererror' && retry) {
|
||||
send(false);
|
||||
} else {
|
||||
deferred.reject(xhr, status, error);
|
||||
}
|
||||
});
|
||||
};
|
||||
send(true);
|
||||
|
||||
var promise = deferred.promise();
|
||||
promise.abort = function () {
|
||||
current.abort();
|
||||
};
|
||||
return promise;
|
||||
}
|
||||
|
||||
function errorMessage(xhr, fallback) {
|
||||
var json = xhr && xhr.responseJSON;
|
||||
if (json && json.data && json.data.message) {
|
||||
return json.data.message;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Settings */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
var Settings = {
|
||||
pending: {},
|
||||
sequence: 0,
|
||||
// Saves run one at a time so each reads the settings the last one wrote.
|
||||
queue: $.Deferred().resolve().promise(),
|
||||
|
||||
init: function () {
|
||||
$(document).on('change', '[data-wps-setting]', function () {
|
||||
Settings.save($(this));
|
||||
});
|
||||
// "Select all" and "Clear" for long checkbox lists: one save for the lot.
|
||||
$(document).on('click', '[data-wps-check-all], [data-wps-check-none]', function () {
|
||||
var all = this.hasAttribute('data-wps-check-all');
|
||||
var $group = $('#' + $(this).attr(all ? 'data-wps-check-all' : 'data-wps-check-none'));
|
||||
$group.find(':checkbox').prop('checked', all);
|
||||
Settings.save($group);
|
||||
});
|
||||
// Forms that change data elsewhere ask first.
|
||||
$(document).on('submit', 'form[data-wps-confirm]', function (event) {
|
||||
if (!window.confirm($(this).attr('data-wps-confirm'))) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
valueOf: function ($input) {
|
||||
if ($input.is('[data-wps-multi]')) {
|
||||
return $input.find(':checkbox:checked').map(function () {
|
||||
return this.value;
|
||||
}).get();
|
||||
}
|
||||
if ($input.is(':checkbox')) {
|
||||
return $input.is(':checked') ? '1' : '0';
|
||||
}
|
||||
return $input.val();
|
||||
},
|
||||
|
||||
status: function (key, state, text, keep) {
|
||||
var $status = $('[data-wps-status="' + key + '"]');
|
||||
$status.removeClass('is-saving is-saved is-error').addClass(state ? 'is-' + state : '').text(text || '');
|
||||
clearTimeout($status.data('timer'));
|
||||
// "Reload the page" stays until the next change.
|
||||
if (state === 'saved' && !keep) {
|
||||
$status.data('timer', setTimeout(function () {
|
||||
$status.removeClass('is-saved').text('');
|
||||
}, 2000));
|
||||
}
|
||||
},
|
||||
|
||||
save: function ($input) {
|
||||
var key = $input.data('wps-setting');
|
||||
var value = this.valueOf($input);
|
||||
var previous = $input.data('wps-saved');
|
||||
var seq = ++this.sequence;
|
||||
|
||||
if (previous === undefined) {
|
||||
previous = $input.is(':checkbox') ? ($input.is(':checked') ? '0' : '1') : value;
|
||||
}
|
||||
|
||||
this.pending[key] = seq;
|
||||
this.status(key, 'saving', i18n.saving);
|
||||
|
||||
var run = function () {
|
||||
return post('wpstarter_save_setting', { key: key, value: value })
|
||||
.done(done)
|
||||
.fail(failed);
|
||||
};
|
||||
var done = function (response) {
|
||||
if (Settings.pending[key] !== seq) {
|
||||
return; // A newer save for this key is queued.
|
||||
}
|
||||
if (!response || !response.success) {
|
||||
Settings.fail($input, key, previous, i18n.saveFailed);
|
||||
return;
|
||||
}
|
||||
var saved = response.data.value;
|
||||
if ($input.is('[data-wps-multi]')) {
|
||||
var chosen = $.map(saved || [], String);
|
||||
$input.find(':checkbox').each(function () {
|
||||
this.checked = $.inArray(this.value, chosen) !== -1;
|
||||
});
|
||||
} else if ($input.is(':checkbox')) {
|
||||
$input.data('wps-saved', saved ? '1' : '0');
|
||||
} else if (saved !== undefined && String(saved) !== String($input.val())) {
|
||||
$input.val(saved); // Show the sanitized value.
|
||||
}
|
||||
if (!$input.is(':checkbox')) {
|
||||
$input.data('wps-saved', $input.val());
|
||||
}
|
||||
var reload = !!response.data.reload;
|
||||
var message = reload && response.data.message ? response.data.message : i18n.saved;
|
||||
Settings.status(key, 'saved', message, reload);
|
||||
speak(message);
|
||||
$(document).trigger('wpstarter:setting-saved', [key, saved]);
|
||||
};
|
||||
var failed = function (xhr) {
|
||||
if (Settings.pending[key] === seq) {
|
||||
Settings.fail($input, key, previous, errorMessage(xhr, i18n.saveFailed));
|
||||
}
|
||||
};
|
||||
|
||||
// A failed save must not block the ones after it.
|
||||
this.queue = this.queue.then(run, run);
|
||||
|
||||
if ($input.is(':checkbox')) {
|
||||
$input.closest('[data-setting-card]').filter(function () {
|
||||
return $(this).data('setting-card') === key;
|
||||
}).toggleClass('is-on', $input.is(':checked'));
|
||||
}
|
||||
},
|
||||
|
||||
fail: function ($input, key, previous, message) {
|
||||
if ($input.is(':checkbox')) {
|
||||
var on = previous === '1' || previous === true;
|
||||
$input.prop('checked', on);
|
||||
$input.closest('[data-setting-card]').toggleClass('is-on', on);
|
||||
}
|
||||
this.status(key, 'error', message);
|
||||
speak(message, 'assertive');
|
||||
}
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Expandable panels and tokens */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
var Panels = {
|
||||
toggle: function ($button) {
|
||||
if (!$button.length) {
|
||||
return;
|
||||
}
|
||||
var expanded = $button.attr('aria-expanded') === 'true';
|
||||
var panel = document.getElementById($button.attr('aria-controls'));
|
||||
$button.attr('aria-expanded', expanded ? 'false' : 'true');
|
||||
if (panel) {
|
||||
panel.hidden = expanded;
|
||||
}
|
||||
$button.closest('.wps-setting').toggleClass('is-expanded', !expanded);
|
||||
},
|
||||
|
||||
init: function () {
|
||||
$(document).on('click', '.wps-setting__expand', function (event) {
|
||||
event.stopPropagation();
|
||||
Panels.toggle($(this));
|
||||
});
|
||||
|
||||
// Mouse convenience: clicking anywhere on the header opens/closes the
|
||||
// options. The switch (and any other control) keeps its own behaviour;
|
||||
// keyboard users use the Options button.
|
||||
$(document).on('click', '[data-wps-panel-toggle]', function (event) {
|
||||
if ($(event.target).closest('input, button, a, label, select, textarea, .wps-switch').length) {
|
||||
return;
|
||||
}
|
||||
if (window.getSelection && String(window.getSelection()).length) {
|
||||
return; // Let people select text.
|
||||
}
|
||||
Panels.toggle($(this).find('.wps-setting__expand').first());
|
||||
});
|
||||
|
||||
$(document).on('click', '.wps-token', function () {
|
||||
var input = document.getElementById($(this).data('target'));
|
||||
var token = String($(this).data('token'));
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
var start = typeof input.selectionStart === 'number' ? input.selectionStart : input.value.length;
|
||||
var end = typeof input.selectionEnd === 'number' ? input.selectionEnd : input.value.length;
|
||||
input.value = input.value.slice(0, start) + token + input.value.slice(end);
|
||||
input.focus();
|
||||
input.setSelectionRange(start + token.length, start + token.length);
|
||||
$(input).trigger('change');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Media fields: choose a Media Library picture */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
var MediaField = {
|
||||
init: function () {
|
||||
$(document).on('click', '.wps-media__choose', function () {
|
||||
MediaField.open($(this).closest('[data-wps-media]'));
|
||||
});
|
||||
$(document).on('click', '.wps-media__remove', function () {
|
||||
MediaField.set($(this).closest('[data-wps-media]'), 0, '');
|
||||
$(this).siblings('.wps-media__choose').trigger('focus');
|
||||
});
|
||||
// A saved 0 means the choice was not a picture.
|
||||
$(document).on('wpstarter:setting-saved', function (event, key, value) {
|
||||
var $field = $('[data-wps-media]').has('[data-wps-setting="' + key + '"]');
|
||||
if ($field.length && !parseInt(value, 10)) {
|
||||
MediaField.preview($field, '');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
open: function ($field) {
|
||||
if (!wp || !wp.media) {
|
||||
return;
|
||||
}
|
||||
var frame = $field.data('wps-frame');
|
||||
if (!frame) {
|
||||
frame = wp.media({
|
||||
title: i18n.chooseImage,
|
||||
library: { type: 'image' },
|
||||
multiple: false,
|
||||
button: { text: i18n.useImage }
|
||||
});
|
||||
frame.on('select', function () {
|
||||
var item = frame.state().get('selection').first();
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
var data = item.toJSON();
|
||||
var url = data.sizes && data.sizes.thumbnail ? data.sizes.thumbnail.url : data.url;
|
||||
MediaField.set($field, data.id, url);
|
||||
});
|
||||
$field.data('wps-frame', frame);
|
||||
}
|
||||
frame.open();
|
||||
},
|
||||
|
||||
set: function ($field, id, url) {
|
||||
this.preview($field, url);
|
||||
$field.find('[data-wps-setting]').val(String(id || 0)).trigger('change');
|
||||
},
|
||||
|
||||
preview: function ($field, url) {
|
||||
var $img = $field.find('.wps-media__preview');
|
||||
if (url) {
|
||||
$img.attr('src', url).prop('hidden', false);
|
||||
} else {
|
||||
$img.removeAttr('src').prop('hidden', true);
|
||||
}
|
||||
$field.find('.wps-media__remove').prop('hidden', !url);
|
||||
}
|
||||
};
|
||||
|
||||
// For the plugin's own admin scripts, which load after this one
|
||||
// (wpstarter_admin_enqueue): the same AJAX, screen reader and error helpers.
|
||||
cfg.api = { post: post, speak: speak, errorMessage: errorMessage };
|
||||
|
||||
$(function () {
|
||||
Settings.init();
|
||||
Panels.init();
|
||||
MediaField.init();
|
||||
});
|
||||
})(jQuery, window.wp, window.wpstarterAdmin);
|
||||
Reference in new issue
Block a user