press-this.js
/**
* PressThis App
*
*/
(function ($, window) {
var PressThis = function () {
var editor, $mediaList, $mediaThumbWrap,
$window = $(window),
$document = $(document),
saveAlert = false,
sidebarIsOpen = false,
settings = window.wpPressThisConfig || {},
data = window.wpPressThisData || {},
smallestWidth = 128,
hasSetFocus = false,
catsCache = [],
isOffScreen = 'is-off-screen',
isHidden = 'is-hidden',
offscreenHidden = isOffScreen + ' ' + isHidden,
iOS = /iPad|iPod|iPhone/.test(window.navigator.userAgent),
$textEditor = $('#pressthis'),
textEditor = $textEditor[0],
textEditorMinHeight = 600,
textLength = 0,
transitionEndEvent = (function () {
var style = document.documentElement.style;
if (typeof style.transition !== 'undefined') {
return 'transitionend';
}
if (typeof style.WebkitTransition !== 'undefined') {
return 'webkitTransitionEnd';
}
return false;
}());
/* ***************************************************************
* HELPER FUNCTIONS
*************************************************************** */
/**
* Emulates our PHP __() gettext function, powered by the strings exported in pressThisL10n.
*
* @param key string Key of the string to be translated, as found in pressThisL10n.
* @returns string Original or translated string, or empty string if no key.
*/
function __(key) {
if (key && window.pressThisL10n) {
return window.pressThisL10n[key] || key;
}
return key || '';
}
/**
* Allow only HTTP or protocol relative URLs.
*
* @param url string The URL.
* @returns string Processed URL.
*/
function checkUrl(url) {
url = $.trim(url || '');
if (/^(?:https?:)?\/\//.test(url)) {
url = wp.sanitize.stripTags(url);
return url.replace(/["\\]+/g, '');
}
return '';
}
/**
* Show UX spinner
*/
function showSpinner() {
$('.spinner').addClass('is-active');
$('.post-actions button').attr('disabled', 'disabled');
}
/**
* Hide UX spinner
*/
function hideSpinner() {
$('.spinner').removeClass('is-active');
$('.post-actions button').removeAttr('disabled');
}
function textEditorResize(reset) {
var pageYOffset, height;
if (editor && !editor.isHidden()) {
return;
}
reset = (reset === 'reset') || (textLength && textLength > textEditor.value.length);
height = textEditor.style.height;
if (reset) {
pageYOffset = window.pageYOffset;
textEditor.style.height = 'auto';
textEditor.style.height = Math.max(textEditor.scrollHeight, textEditorMinHeight) + 'px';
window.scrollTo(window.pageXOffset, pageYOffset);
} else if (parseInt(textEditor.style.height, 10) < textEditor.scrollHeight) {
textEditor.style.height = textEditor.scrollHeight + 'px';
}
textLength = textEditor.value.length;
}
function mceGetCursorOffset() {
if (!editor) {
return false;
}
var node = editor.selection.getNode(),
range, view, offset;
if (editor.wp && editor.wp.getView && (view = editor.wp.getView(node))) {
offset = view.getBoundingClientRect();
} else {
range = editor.selection.getRng();
try {
offset = range.getClientRects()[0];
} catch (er) { }
if (!offset) {
offset = node.getBoundingClientRect();
}
}
return offset.height ? offset : false;
}
// Make sure the caret is always visible.
function mceKeyup(event) {
var VK = window.tinymce.util.VK,
key = event.keyCode;
// Bail on special keys.
if (key <= 47 && !(key === VK.SPACEBAR || key === VK.ENTER || key === VK.DELETE || key === VK.BACKSPACE || key === VK.UP || key === VK.LEFT || key === VK.DOWN || key === VK.UP)) {
return;
// OS keys, function keys, num lock, scroll lock
} else if ((key >= 91 && key <= 93) || (key >= 112 && key <= 123) || key === 144 || key === 145) {
return;
}
mceScroll(key);
}
function mceScroll(key) {
var cursorTop, cursorBottom, editorBottom,
offset = mceGetCursorOffset(),
bufferTop = 50,
bufferBottom = 65,
VK = window.tinymce.util.VK;
if (!offset) {
return;
}
cursorTop = offset.top + editor.iframeElement.getBoundingClientRect().top;
cursorBottom = cursorTop + offset.height;
cursorTop = cursorTop - bufferTop;
cursorBottom = cursorBottom + bufferBottom;
editorBottom = $window.height();
// Don't scroll if the node is taller than the visible part of the editor
if (editorBottom < offset.height) {
return;
}
if (cursorTop < 0 && (key === VK.UP || key === VK.LEFT || key === VK.BACKSPACE)) {
window.scrollTo(window.pageXOffset, cursorTop + window.pageYOffset);
} else if (cursorBottom > editorBottom) {
window.scrollTo(window.pageXOffset, cursorBottom + window.pageYOffset - editorBottom);
}
}
/**
* Replace emoji images with chars and sanitize the text content.
*/
function getTitleText() {
var $element = $('#title-container');
$element.find('img.emoji').each(function () {
var $image = $(this);
$image.replaceWith($('<span>').text($image.attr('alt')));
});
return wp.sanitize.stripTagsAndEncodeText($element.text());
}
/**
* Prepare the form data for saving.
*/
function prepareFormData() {
var $form = $('#pressthis-form'),
$input = $('<input type="hidden" name="post_category[]" value="">');
editor && editor.save();
$('#post_title').val(getTitleText());
// Make sure to flush out the tags with tagBox before saving
if (window.tagBox) {
$('div.tagsdiv').each(function () {
window.tagBox.flushTags(this, false, 1);
});
}
// Get selected categories
$('.categories-select .category').each(function (i, element) {
var $cat = $(element);
if ($cat.hasClass('selected')) {
// Have to append a node as we submit the actual form on preview
$form.append($input.clone().val($cat.attr('data-term-id') || ''));
}
});
}
/**
* Submit the post form via AJAX, and redirect to the proper screen if published vs saved as a draft.
*
* @param action string publish|draft
*/
function submitPost(action) {
var data;
saveAlert = false;
showSpinner();
if ('publish' === action) {
$('#post_status').val('publish');
}
prepareFormData();
data = $('#pressthis-form').serialize();
$.ajax({
type: 'post',
url: window.ajaxurl,
data: data
}).always(function () {
hideSpinner();
clearNotices();
$('.publish-button').removeClass('is-saving');
}).done(function (response) {
if (!response.success) {
renderError(response.data.errorMessage);
} else if (response.data.redirect) {
if (window.opener && (settings.redirInParent || response.data.force)) {
try {
window.opener.location.href = response.data.redirect;
window.setTimeout(function () {
window.self.close();
}, 200);
} catch (er) {
window.location.href = response.data.redirect;
}
} else {
window.location.href = response.data.redirect;
}
}
}).fail(function () {
renderError(__('serverError'));
});
}
/**
* Inserts the media a user has selected from the presented list inside the editor, as an image or embed, based on type
*
* @param type string img|embed
* @param src string Source URL
* @param link string Optional destination link, for images (defaults to src)
*/
function insertSelectedMedia($element) {
var src, link, newContent = '';
src = checkUrl($element.attr('data-wp-src') || '');
link = checkUrl(data.u);
if ($element.hasClass('is-image')) {
if (!link) {
link = src;
}
newContent = '<a href="' + link + '"><img class="alignnone size-full" src="' + src + '" alt="" /></a>';
} else {
newContent = '[embed]' + src + '[/embed]';
}
if (editor && !editor.isHidden()) {
if (!hasSetFocus) {
editor.setContent('<p>' + newContent + '</p>' + editor.getContent());
} else {
editor.execCommand('mceInsertContent', false, newContent);
}
} else if (window.QTags) {
window.QTags.insertContent(newContent);
}
}
/**
* Save a new user-generated category via AJAX
*/
function saveNewCategory() {
var data,
name = $('#new-category').val();
if (!name) {
return;
}
data = {
action: 'press-this-add-category',
post_id: $('#post_ID').val() || 0,
name: name,
new_cat_nonce: $('#_ajax_nonce-add-category').val() || '',
parent: $('#new-category-parent').val() || 0
};
$.post(window.ajaxurl, data, function (response) {
if (!response.success) {
renderError(response.data.errorMessage);
} else {
var $parent, $ul,
$wrap = $('ul.categories-select');
$.each(response.data, function (i, newCat) {
var $node = $('<li>').append($('<div class="category selected" tabindex="0" role="checkbox" aria-checked="true">')
.attr('data-term-id', newCat.term_id)
.text(newCat.name));
if (newCat.parent) {
if (!$ul || !$ul.length) {
$parent = $wrap.find('div[data-term-id="' + newCat.parent + '"]').parent();
$ul = $parent.find('ul.children:first');
if (!$ul.length) {
$ul = $('<ul class="children">').appendTo($parent);
}
}
$ul.prepend($node);
} else {
$wrap.prepend($node);
}
$node.focus();
});
refreshCatsCache();
}
});
}
/* ***************************************************************
* RENDERING FUNCTIONS
*************************************************************** */
/**
* Hide the form letting users enter a URL to be scanned, if a URL was already passed.
*/
function renderToolsVisibility() {
if (data.hasData) {
$('#scanbar').hide();
}
}
/**
* Render error notice
*
* @param msg string Notice/error message
* @param error string error|notice CSS class for display
*/
function renderNotice(msg, error) {
var $alerts = $('.editor-wrapper div.alerts'),
className = error ? 'is-error' : 'is-notice';
$alerts.append($('<p class="alert ' + className + '">').text(msg));
}
/**
* Render error notice
*
* @param msg string Error message
*/
function renderError(msg) {
renderNotice(msg, true);
}
function clearNotices() {
$('div.alerts').empty();
}
/**
* Render notices on page load, if any already
*/
function renderStartupNotices() {
// Render errors sent in the data, if any
if (data.errors) {
$.each(data.errors, function (i, msg) {
renderError(msg);
});
}
}
/**
* Add an image to the list of found images.
*/
function addImg(src, displaySrc, i) {
var $element = $mediaThumbWrap.clone().addClass('is-image');
$element.attr('data-wp-src', src).css('background-image', 'url(' + displaySrc + ')')
.find('span').text(__('suggestedImgAlt').replace('%d', i + 1));
$mediaList.append($element);
}
/**
* Render the detected images and embed for selection, if any
*/
function renderDetectedMedia() {
var found = 0;
$mediaList = $('ul.media-list');
$mediaThumbWrap = $('<li class="suggested-media-thumbnail" tabindex="0"><span class="screen-reader-text"></span></li>');
if (data._embeds) {
$.each(data._embeds, function (i, src) {
var displaySrc = '',
cssClass = '',
$element = $mediaThumbWrap.clone().addClass('is-embed');
src = checkUrl(src);
if (src.indexOf('youtube.com/') > -1) {
displaySrc = 'https://i.ytimg.com/vi/' + src.replace(/.+v=([^&]+).*/, '$1') + '/hqdefault.jpg';
cssClass += ' is-video';
} else if (src.indexOf('youtu.be/') > -1) {
displaySrc = 'https://i.ytimg.com/vi/' + src.replace(/\/([^\/])$/, '$1') + '/hqdefault.jpg';
cssClass += ' is-video';
} else if (src.indexOf('dailymotion.com') > -1) {
displaySrc = src.replace('/video/', '/thumbnail/video/');
cssClass += ' is-video';
} else if (src.indexOf('soundcloud.com') > -1) {
cssClass += ' is-audio';
} else if (src.indexOf('twitter.com') > -1) {
cssClass += ' is-tweet';
} else {
cssClass += ' is-video';
}
$element.attr('data-wp-src', src).find('span').text(__('suggestedEmbedAlt').replace('%d', i + 1));
if (displaySrc) {
$element.css('background-image', 'url(' + displaySrc + ')');
}
$mediaList.append($element);
found++;
});
}
if (data._images) {
$.each(data._images, function (i, src) {
var displaySrc, img = new Image();
src = checkUrl(src);
displaySrc = src.replace(/^(http[^\?]+)(\?.*)?$/, '$1');
if (src.indexOf('files.wordpress.com/') > -1) {
displaySrc = displaySrc.replace(/\?.*$/, '') + '?w=' + smallestWidth;
} else if (src.indexOf('gravatar.com/') > -1) {
displaySrc = displaySrc.replace(/\?.*$/, '') + '?s=' + smallestWidth;
} else {
displaySrc = src;
}
img.onload = function () {
if ((img.width && img.width < 256) ||
(img.height && img.height < 128)) {
return;
}
addImg(src, displaySrc, i);
};
img.src = src;
found++;
});
}
if (found) {
$('.media-list-container').addClass('has-media');
}
}
/* ***************************************************************
* MONITORING FUNCTIONS
*************************************************************** */
/**
* Interactive navigation behavior for the options modal (post format, tags, categories)
*/
function monitorOptionsModal() {
var $postOptions = $('.post-options'),
$postOption = $('.post-option'),
$settingModal = $('.setting-modal'),
$modalClose = $('.modal-close');
$postOption.on('click', function () {
var index = $(this).index(),
$targetSettingModal = $settingModal.eq(index);
$postOptions.addClass(isOffScreen)
.one(transitionEndEvent, function () {
$(this).addClass(isHidden);
});
$targetSettingModal.removeClass(offscreenHidden)
.one(transitionEndEvent, function () {
$(this).find('.modal-close').focus();
});
});
$modalClose.on('click', function () {
var $targetSettingModal = $(this).parent(),
index = $targetSettingModal.index();
$postOptions.removeClass(offscreenHidden);
$targetSettingModal.addClass(isOffScreen);
if (transitionEndEvent) {
$targetSettingModal.one(transitionEndEvent, function () {
$(this).addClass(isHidden);
$postOption.eq(index - 1).focus();
});
} else {
setTimeout(function () {
$targetSettingModal.addClass(isHidden);
$postOption.eq(index - 1).focus();
}, 350);
}
});
}
/**
* Interactive behavior for the sidebar toggle, to show the options modals
*/
function openSidebar() {
sidebarIsOpen = true;
$('.options').removeClass('closed').addClass('open');
$('.press-this-actions, #scanbar').addClass(isHidden);
$('.options-panel-back').removeClass(isHidden);
$('.options-panel').removeClass(offscreenHidden)
.one(transitionEndEvent, function () {
$('.post-option:first').focus();
});
}
function closeSidebar() {
sidebarIsOpen = false;
$('.options').removeClass('open').addClass('closed');
$('.options-panel-back').addClass(isHidden);
$('.press-this-actions, #scanbar').removeClass(isHidden);
$('.options-panel').addClass(isOffScreen)
.one(transitionEndEvent, function () {
$(this).addClass(isHidden);
// Reset to options list
$('.post-options').removeClass(offscreenHidden);
$('.setting-modal').addClass(offscreenHidden);
});
}
/**
* Interactive behavior for the post title's field placeholder
*/
function monitorPlaceholder() {
var $titleField = $('#title-container'),
$placeholder = $('.post-title-placeholder');
$titleField.on('focus', function () {
$placeholder.addClass('is-hidden');
}).on('blur', function () {
if (!$titleField.text() && !$titleField.html()) {
$placeholder.removeClass('is-hidden');
}
}).on('keyup', function () {
saveAlert = true;
}).on('paste', function (event) {
var text, range,
clipboard = event.originalEvent.clipboardData || window.clipboardData;
if (clipboard) {
try {
text = clipboard.getData('Text') || clipboard.getData('text/plain');
if (text) {
text = $.trim(text.replace(/\s+/g, ' '));
if (window.getSelection) {
range = window.getSelection().getRangeAt(0);
if (range) {
if (!range.collapsed) {
range.deleteContents();
}
range.insertNode(document.createTextNode(text));
}
} else if (document.selection) {
range = document.selection.createRange();
if (range) {
range.text = text;
}
}
}
} catch (er) { }
event.preventDefault();
}
saveAlert = true;
setTimeout(function () {
$titleField.text(getTitleText());
}, 50);
});
if ($titleField.text() || $titleField.html()) {
$placeholder.addClass('is-hidden');
}
}
function toggleCatItem($element) {
if ($element.hasClass('selected')) {
$element.removeClass('selected').attr('aria-checked', 'false');
} else {
$element.addClass('selected').attr('aria-checked', 'true');
}
}
function monitorCatList() {
$('.categories-select').on('click.press-this keydown.press-this', function (event) {
var $element = $(event.target);
if ($element.is('div.category')) {
if (event.type === 'keydown' && event.keyCode !== 32) {
return;
}
toggleCatItem($element);
event.preventDefault();
}
});
}
function splitButtonClose() {
$('.split-button').removeClass('is-open');
$('.split-button-toggle').attr('aria-expanded', 'false');
}
/* ***************************************************************
* PROCESSING FUNCTIONS
*************************************************************** */
/**
* Calls all the rendring related functions to happen on page load
*/
function render() {
// We're on!
renderToolsVisibility();
renderDetectedMedia();
renderStartupNotices();
if (window.tagBox) {
window.tagBox.init();
}
// iOS doesn't fire click events on "standard" elements without this...
if (iOS) {
$(document.body).css('cursor', 'pointer');
}
}
/**
* Set app events and other state monitoring related code.
*/
function monitor() {
var $splitButton = $('.split-button');
$document.on('tinymce-editor-init', function (event, ed) {
editor = ed;
editor.on('nodechange', function () {
hasSetFocus = true;
});
editor.on('focus', function () {
splitButtonClose();
});
editor.on('show', function () {
setTimeout(function () {
editor.execCommand('wpAutoResize');
}, 300);
});
editor.on('hide', function () {
setTimeout(function () {
textEditorResize('reset');
}, 100);
});
editor.on('keyup', mceKeyup);
editor.on('undo redo', mceScroll);
}).on('click.press-this keypress.press-this', '.suggested-media-thumbnail', function (event) {
if (event.type === 'click' || event.keyCode === 13) {
insertSelectedMedia($(this));
}
}).on('click.press-this', function (event) {
if (!$(event.target).closest('button').hasClass('split-button-toggle')) {
splitButtonClose();
}
});
// Publish, Draft and Preview buttons
$('.post-actions').on('click.press-this', function (event) {
var location,
$target = $(event.target),
$button = $target.closest('button');
if ($button.length) {
if ($button.hasClass('draft-button')) {
$('.publish-button').addClass('is-saving');
submitPost('draft');
} else if ($button.hasClass('publish-button')) {
$button.addClass('is-saving');
if (window.history.replaceState) {
location = window.location.href;
location += (location.indexOf('?') !== -1) ? '&' : '?';
location += 'wp-press-this-reload=true';
window.history.replaceState(null, null, location);
}
submitPost('publish');
} else if ($button.hasClass('preview-button')) {
prepareFormData();
window.opener && window.opener.focus();
$('#wp-preview').val('dopreview');
$('#pressthis-form').attr('target', '_blank').submit().attr('target', '');
$('#wp-preview').val('');
} else if ($button.hasClass('standard-editor-button')) {
$('.publish-button').addClass('is-saving');
$('#pt-force-redirect').val('true');
submitPost('draft');
} else if ($button.hasClass('split-button-toggle')) {
if ($splitButton.hasClass('is-open')) {
$splitButton.removeClass('is-open');
$button.attr('aria-expanded', 'false');
} else {
$splitButton.addClass('is-open');
$button.attr('aria-expanded', 'true');
}
}
}
});
monitorOptionsModal();
monitorPlaceholder();
monitorCatList();
$('.options').on('click.press-this', function () {
if ($(this).hasClass('open')) {
closeSidebar();
} else {
openSidebar();
}
});
// Close the sidebar when focus moves outside of it.
$('.options-panel, .options-panel-back').on('focusout.press-this', function () {
setTimeout(function () {
var node = document.activeElement,
$node = $(node);
if (sidebarIsOpen && node && !$node.hasClass('options-panel-back') &&
(node.nodeName === 'BODY' ||
(!$node.closest('.options-panel').length &&
!$node.closest('.options').length))) {
closeSidebar();
}
}, 50);
});
$('#post-formats-select input').on('change', function () {
var $this = $(this);
if ($this.is(':checked')) {
$('#post-option-post-format').text($('label[for="' + $this.attr('id') + '"]').text() || '');
}
});
$window.on('beforeunload.press-this', function () {
if (saveAlert || (editor && editor.isDirty())) {
return __('saveAlert');
}
}).on('resize.press-this', function () {
if (!editor || editor.isHidden()) {
textEditorResize('reset');
}
});
$('button.add-cat-toggle').on('click.press-this', function () {
var $this = $(this);
$this.toggleClass('is-toggled');
$this.attr('aria-expanded', 'false' === $this.attr('aria-expanded') ? 'true' : 'false');
$('.setting-modal .add-category, .categories-search-wrapper').toggleClass('is-hidden');
});
$('button.add-cat-submit').on('click.press-this', saveNewCategory);
$('.categories-search').on('keyup.press-this', function () {
var search = $(this).val().toLowerCase() || '';
// Don't search when less thasn 3 extended ASCII chars
if (/[\x20-\xFF]+/.test(search) && search.length < 2) {
return;
}
$.each(catsCache, function (i, cat) {
cat.node.removeClass('is-hidden searched-parent');
});
if (search) {
$.each(catsCache, function (i, cat) {
if (cat.text.indexOf(search) === -1) {
cat.node.addClass('is-hidden');
} else {
cat.parents.addClass('searched-parent');
}
});
}
});
$textEditor.on('focus.press-this input.press-this propertychange.press-this', textEditorResize);
return true;
}
function refreshCatsCache() {
$('.categories-select').find('li').each(function () {
var $this = $(this);
catsCache.push({
node: $this,
parents: $this.parents('li'),
text: $this.children('.category').text().toLowerCase()
});
});
}
// Let's go!
$document.ready(function () {
render();
monitor();
refreshCatsCache();
});
// Expose public methods?
return {
renderNotice: renderNotice,
renderError: renderError
};
};
window.wp = window.wp || {};
window.wp.pressThis = new PressThis();
}(jQuery, window));