5 ( function( $, window ) {
6 var PressThis = function() {
7 var editor, $mediaList, $mediaThumbWrap,
9 editLinkVisible = false,
10 textarea = document.createElement( 'textarea' ),
11 sidebarIsOpen = false,
12 settings = window.wpPressThisConfig || {},
13 data = window.wpPressThisData || {},
17 isOffScreen = 'is-off-screen',
18 isHidden = 'is-hidden',
19 offscreenHidden = isOffScreen + ' ' + isHidden,
20 transitionEndEvent = ( function() {
21 var style = document.documentElement.style;
23 if ( typeof style.transition !== 'undefined' ) {
24 return 'transitionend';
27 if ( typeof style.WebkitTransition !== 'undefined' ) {
28 return 'webkitTransitionEnd';
34 /* ***************************************************************
36 *************************************************************** */
39 * Emulates our PHP __() gettext function, powered by the strings exported in pressThisL10n.
41 * @param key string Key of the string to be translated, as found in pressThisL10n.
42 * @returns string Original or translated string, or empty string if no key.
45 if ( key && window.pressThisL10n ) {
46 return window.pressThisL10n[key] || key;
55 * @param string string Text to have the HTML tags striped out of.
56 * @returns string Stripped text.
58 function stripTags( string ) {
59 string = string || '';
62 .replace( /<!--[\s\S]*?(-->|$)/g, '' )
63 .replace( /<(script|style)[^>]*>[\s\S]*?(<\/\1>|$)/ig, '' )
64 .replace( /<\/?[a-z][\s\S]*?(>|$)/ig, '' );
68 * Strip HTML tags and convert HTML entities.
70 * @param text string Text.
71 * @returns string Sanitized text.
73 function sanitizeText( text ) {
74 var _text = stripTags( text );
77 textarea.innerHTML = _text;
78 _text = stripTags( textarea.value );
85 * Allow only HTTP or protocol relative URLs.
87 * @param url string The URL.
88 * @returns string Processed URL.
90 function checkUrl( url ) {
91 url = $.trim( url || '' );
93 if ( /^(?:https?:)?\/\//.test( url ) ) {
94 url = stripTags( url );
95 return url.replace( /["\\]+/g, '' );
104 function showSpinner() {
105 $( '.spinner' ).addClass( 'is-active' );
106 $( '.post-actions button' ).attr( 'disabled', 'disabled' );
112 function hideSpinner() {
113 $( '.spinner' ).removeClass( 'is-active' );
114 $( '.post-actions button' ).removeAttr( 'disabled' );
118 * Replace emoji images with chars and sanitize the text content.
120 function getTitleText() {
121 var $element = $( '#title-container' );
123 $element.find( 'img.emoji' ).each( function() {
124 var $image = $( this );
125 $image.replaceWith( $( '<span>' ).text( $image.attr( 'alt' ) ) );
128 return sanitizeText( $element.text() );
132 * Prepare the form data for saving.
134 function prepareFormData() {
135 var $form = $( '#pressthis-form' ),
136 $input = $( '<input type="hidden" name="post_category[]" value="">' );
138 editor && editor.save();
140 $( '#post_title' ).val( getTitleText() );
142 // Make sure to flush out the tags with tagBox before saving
143 if ( window.tagBox ) {
144 $( 'div.tagsdiv' ).each( function() {
145 window.tagBox.flushTags( this, false, 1 );
149 // Get selected categories
150 $( '.categories-select .category' ).each( function( i, element ) {
151 var $cat = $( element );
153 if ( $cat.hasClass( 'selected' ) ) {
154 // Have to append a node as we submit the actual form on preview
155 $form.append( $input.clone().val( $cat.attr( 'data-term-id' ) || '' ) );
161 * Submit the post form via AJAX, and redirect to the proper screen if published vs saved as a draft.
163 * @param action string publish|draft
165 function submitPost( action ) {
167 keepFocus = $( document.activeElement ).hasClass( 'draft-button' );
172 if ( 'publish' === action ) {
173 $( '#post_status' ).val( 'publish' );
177 data = $( '#pressthis-form' ).serialize();
183 }).always( function() {
186 }).done( function( response ) {
189 if ( ! response.success ) {
190 renderError( response.data.errorMessage );
191 } else if ( response.data.redirect ) {
192 if ( window.opener && settings.redirInParent ) {
194 window.opener.location.href = response.data.redirect;
199 window.location.href = response.data.redirect;
201 } else if ( response.data.postSaved ) {
202 $link = $( '.edit-post-link' );
203 $button = $( '.draft-button' );
204 editLinkVisible = true;
206 $button.fadeOut( 200, function() {
207 $button.removeClass( 'is-saving' );
208 $link.fadeIn( 200, function() {
209 var active = document.activeElement;
210 // Different browsers move the focus to different places when the button is disabled.
211 if ( keepFocus && ( active === $button[0] || $( active ).hasClass( 'post-actions' ) || active.nodeName === 'BODY' ) ) {
217 }).fail( function() {
218 renderError( __( 'serverError' ) );
222 function resetDraftButton() {
223 if ( editLinkVisible ) {
224 editLinkVisible = false;
226 $( '.edit-post-link' ).fadeOut( 200, function() {
227 $( '.draft-button' ).removeClass( 'is-saving' ).fadeIn( 200 );
233 * Inserts the media a user has selected from the presented list inside the editor, as an image or embed, based on type
235 * @param type string img|embed
236 * @param src string Source URL
237 * @param link string Optional destination link, for images (defaults to src)
239 function insertSelectedMedia( $element ) {
240 var src, link, newContent = '';
246 src = checkUrl( $element.attr( 'data-wp-src' ) || '' );
247 link = checkUrl( data.u );
249 if ( $element.hasClass( 'is-image' ) ) {
254 newContent = '<a href="' + link + '"><img class="alignnone size-full" src="' + src + '" /></a>';
256 newContent = '[embed]' + src + '[/embed]';
259 if ( ! hasSetFocus ) {
260 editor.setContent( '<p>' + newContent + '</p>' + editor.getContent() );
262 editor.execCommand( 'mceInsertContent', false, newContent );
267 * Save a new user-generated category via AJAX
269 function saveNewCategory() {
271 name = $( '#new-category' ).val();
278 action: 'press-this-add-category',
279 post_id: $( '#post_ID' ).val() || 0,
281 new_cat_nonce: $( '#_ajax_nonce-add-category' ).val() || '',
282 parent: $( '#new-category-parent' ).val() || 0
285 $.post( window.ajaxurl, data, function( response ) {
286 if ( ! response.success ) {
287 renderError( response.data.errorMessage );
290 $wrap = $( 'ul.categories-select' );
292 $.each( response.data, function( i, newCat ) {
293 var $node = $( '<li>' ).append( $( '<div class="category selected" tabindex="0" role="checkbox" aria-checked="true">' )
294 .attr( 'data-term-id', newCat.term_id )
295 .text( newCat.name ) );
297 if ( newCat.parent ) {
298 if ( ! $ul || ! $ul.length ) {
299 $parent = $wrap.find( 'div[data-term-id="' + newCat.parent + '"]' ).parent();
300 $ul = $parent.find( 'ul.children:first' );
302 if ( ! $ul.length ) {
303 $ul = $( '<ul class="children">' ).appendTo( $parent );
307 $ul.prepend( $node );
309 $wrap.prepend( $node );
320 /* ***************************************************************
321 * RENDERING FUNCTIONS
322 *************************************************************** */
325 * Hide the form letting users enter a URL to be scanned, if a URL was already passed.
327 function renderToolsVisibility() {
328 if ( data.hasData ) {
329 $( '#scanbar' ).hide();
334 * Render error notice
336 * @param msg string Notice/error message
337 * @param error string error|notice CSS class for display
339 function renderNotice( msg, error ) {
340 var $alerts = $( '.editor-wrapper div.alerts' ),
341 className = error ? 'is-error' : 'is-notice';
343 $alerts.append( $( '<p class="alert ' + className + '">' ).text( msg ) );
347 * Render error notice
349 * @param msg string Error message
351 function renderError( msg ) {
352 renderNotice( msg, true );
355 function clearNotices() {
356 $( 'div.alerts' ).empty();
360 * Render notices on page load, if any already
362 function renderStartupNotices() {
363 // Render errors sent in the data, if any
365 $.each( data.errors, function( i, msg ) {
372 * Add an image to the list of found images.
374 function addImg( src, displaySrc, i ) {
375 var $element = $mediaThumbWrap.clone().addClass( 'is-image' );
377 $element.attr( 'data-wp-src', src ).css( 'background-image', 'url(' + displaySrc + ')' )
378 .find( 'span' ).text( __( 'suggestedImgAlt' ).replace( '%d', i + 1 ) );
380 $mediaList.append( $element );
384 * Render the detected images and embed for selection, if any
386 function renderDetectedMedia() {
389 $mediaList = $( 'ul.media-list' );
390 $mediaThumbWrap = $( '<li class="suggested-media-thumbnail" tabindex="0"><span class="screen-reader-text"></span></li>' );
392 if ( data._embeds ) {
393 $.each( data._embeds, function ( i, src ) {
396 $element = $mediaThumbWrap.clone().addClass( 'is-embed' );
398 src = checkUrl( src );
400 if ( src.indexOf( 'youtube.com/' ) > -1 ) {
401 displaySrc = 'https://i.ytimg.com/vi/' + src.replace( /.+v=([^&]+).*/, '$1' ) + '/hqdefault.jpg';
402 cssClass += ' is-video';
403 } else if ( src.indexOf( 'youtu.be/' ) > -1 ) {
404 displaySrc = 'https://i.ytimg.com/vi/' + src.replace( /\/([^\/])$/, '$1' ) + '/hqdefault.jpg';
405 cssClass += ' is-video';
406 } else if ( src.indexOf( 'dailymotion.com' ) > -1 ) {
407 displaySrc = src.replace( '/video/', '/thumbnail/video/' );
408 cssClass += ' is-video';
409 } else if ( src.indexOf( 'soundcloud.com' ) > -1 ) {
410 cssClass += ' is-audio';
411 } else if ( src.indexOf( 'twitter.com' ) > -1 ) {
412 cssClass += ' is-tweet';
414 cssClass += ' is-video';
417 $element.attr( 'data-wp-src', src ).find( 'span' ).text( __( 'suggestedEmbedAlt' ).replace( '%d', i + 1 ) );
420 $element.css( 'background-image', 'url(' + displaySrc + ')' );
423 $mediaList.append( $element );
428 if ( data._images ) {
429 $.each( data._images, function( i, src ) {
430 var displaySrc, img = new Image();
432 src = checkUrl( src );
433 displaySrc = src.replace( /^(http[^\?]+)(\?.*)?$/, '$1' );
435 if ( src.indexOf( 'files.wordpress.com/' ) > -1 ) {
436 displaySrc = displaySrc.replace( /\?.*$/, '' ) + '?w=' + smallestWidth;
437 } else if ( src.indexOf( 'gravatar.com/' ) > -1 ) {
438 displaySrc = displaySrc.replace( /\?.*$/, '' ) + '?s=' + smallestWidth;
443 img.onload = function() {
444 if ( ( img.width && img.width < 256 ) ||
445 ( img.height && img.height < 128 ) ) {
450 addImg( src, displaySrc, i );
459 $( '.media-list-container' ).addClass( 'has-media' );
463 /* ***************************************************************
464 * MONITORING FUNCTIONS
465 *************************************************************** */
468 * Interactive navigation behavior for the options modal (post format, tags, categories)
470 function monitorOptionsModal() {
471 var $postOptions = $( '.post-options' ),
472 $postOption = $( '.post-option' ),
473 $settingModal = $( '.setting-modal' ),
474 $modalClose = $( '.modal-close' );
476 $postOption.on( 'click', function() {
477 var index = $( this ).index(),
478 $targetSettingModal = $settingModal.eq( index );
480 $postOptions.addClass( isOffScreen )
481 .one( transitionEndEvent, function() {
482 $( this ).addClass( isHidden );
485 $targetSettingModal.removeClass( offscreenHidden )
486 .one( transitionEndEvent, function() {
487 $( this ).find( '.modal-close' ).focus();
491 $modalClose.on( 'click', function() {
492 var $targetSettingModal = $( this ).parent(),
493 index = $targetSettingModal.index();
495 $postOptions.removeClass( offscreenHidden );
496 $targetSettingModal.addClass( isOffScreen );
498 if ( transitionEndEvent ) {
499 $targetSettingModal.one( transitionEndEvent, function() {
500 $( this ).addClass( isHidden );
501 $postOption.eq( index - 1 ).focus();
504 setTimeout( function() {
505 $targetSettingModal.addClass( isHidden );
506 $postOption.eq( index - 1 ).focus();
513 * Interactive behavior for the sidebar toggle, to show the options modals
515 function openSidebar() {
516 sidebarIsOpen = true;
518 $( '.options' ).removeClass( 'closed' ).addClass( 'open' );
519 $( '.press-this-actions, #scanbar' ).addClass( isHidden );
520 $( '.options-panel-back' ).removeClass( isHidden );
522 $( '.options-panel' ).removeClass( offscreenHidden )
523 .one( transitionEndEvent, function() {
524 $( '.post-option:first' ).focus();
528 function closeSidebar() {
529 sidebarIsOpen = false;
531 $( '.options' ).removeClass( 'open' ).addClass( 'closed' );
532 $( '.options-panel-back' ).addClass( isHidden );
533 $( '.press-this-actions, #scanbar' ).removeClass( isHidden );
535 $( '.options-panel' ).addClass( isOffScreen )
536 .one( transitionEndEvent, function() {
537 $( this ).addClass( isHidden );
538 // Reset to options list
539 $( '.post-options' ).removeClass( offscreenHidden );
540 $( '.setting-modal').addClass( offscreenHidden );
545 * Interactive behavior for the post title's field placeholder
547 function monitorPlaceholder() {
548 var $titleField = $( '#title-container' ),
549 $placeholder = $( '.post-title-placeholder' );
551 $titleField.on( 'focus', function() {
552 $placeholder.addClass( 'is-hidden' );
554 }).on( 'blur', function() {
555 if ( ! $titleField.text() && ! $titleField.html() ) {
556 $placeholder.removeClass( 'is-hidden' );
558 }).on( 'keyup', function() {
560 }).on( 'paste', function( event ) {
562 clipboard = event.originalEvent.clipboardData || window.clipboardData;
566 text = clipboard.getData( 'Text' ) || clipboard.getData( 'text/plain' );
569 text = $.trim( text.replace( /\s+/g, ' ' ) );
571 if ( window.getSelection ) {
572 range = window.getSelection().getRangeAt(0);
575 if ( ! range.collapsed ) {
576 range.deleteContents();
579 range.insertNode( document.createTextNode( text ) );
581 } else if ( document.selection ) {
582 range = document.selection.createRange();
591 event.preventDefault();
596 setTimeout( function() {
597 $titleField.text( getTitleText() );
601 if ( $titleField.text() || $titleField.html() ) {
602 $placeholder.addClass('is-hidden');
606 function toggleCatItem( $element ) {
607 if ( $element.hasClass( 'selected' ) ) {
608 $element.removeClass( 'selected' ).attr( 'aria-checked', 'false' );
610 $element.addClass( 'selected' ).attr( 'aria-checked', 'true' );
614 function monitorCatList() {
615 $( '.categories-select' ).on( 'click.press-this keydown.press-this', function( event ) {
616 var $element = $( event.target );
618 if ( $element.is( 'div.category' ) ) {
619 if ( event.type === 'keydown' && event.keyCode !== 32 ) {
623 toggleCatItem( $element );
624 event.preventDefault();
629 /* ***************************************************************
630 * PROCESSING FUNCTIONS
631 *************************************************************** */
634 * Calls all the rendring related functions to happen on page load
638 renderToolsVisibility();
639 renderDetectedMedia();
640 renderStartupNotices();
642 if ( window.tagBox ) {
643 window.tagBox.init();
648 * Set app events and other state monitoring related code.
651 $( document ).on( 'tinymce-editor-init', function( event, ed ) {
654 editor.on( 'nodechange', function() {
658 }).on( 'click.press-this keypress.press-this', '.suggested-media-thumbnail', function( event ) {
659 if ( event.type === 'click' || event.keyCode === 13 ) {
660 insertSelectedMedia( $( this ) );
664 // Publish, Draft and Preview buttons
665 $( '.post-actions' ).on( 'click.press-this', function( event ) {
666 var $target = $( event.target ),
667 $button = $target.closest( 'button' );
669 if ( $button.length ) {
670 if ( $button.hasClass( 'draft-button' ) ) {
671 $button.addClass( 'is-saving' );
672 submitPost( 'draft' );
673 } else if ( $button.hasClass( 'publish-button' ) ) {
674 submitPost( 'publish' );
675 } else if ( $button.hasClass( 'preview-button' ) ) {
677 window.opener && window.opener.focus();
679 $( '#wp-preview' ).val( 'dopreview' );
680 $( '#pressthis-form' ).attr( 'target', '_blank' ).submit().attr( 'target', '' );
681 $( '#wp-preview' ).val( '' );
683 } else if ( $target.hasClass( 'edit-post-link' ) && window.opener ) {
684 window.opener.focus();
689 monitorOptionsModal();
690 monitorPlaceholder();
693 $( '.options' ).on( 'click.press-this', function() {
694 if ( $( this ).hasClass( 'open' ) ) {
701 // Close the sidebar when focus moves outside of it.
702 $( '.options-panel, .options-panel-back' ).on( 'focusout.press-this', function() {
703 setTimeout( function() {
704 var node = document.activeElement,
707 if ( sidebarIsOpen && node && ! $node.hasClass( 'options-panel-back' ) &&
708 ( node.nodeName === 'BODY' ||
709 ( ! $node.closest( '.options-panel' ).length &&
710 ! $node.closest( '.options' ).length ) ) ) {
717 $( '#post-formats-select input' ).on( 'change', function() {
718 var $this = $( this );
720 if ( $this.is( ':checked' ) ) {
721 $( '#post-option-post-format' ).text( $( 'label[for="' + $this.attr( 'id' ) + '"]' ).text() || '' );
725 $( window ).on( 'beforeunload.press-this', function() {
726 if ( saveAlert || ( editor && editor.isDirty() ) ) {
727 return __( 'saveAlert' );
731 $( 'button.add-cat-toggle' ).on( 'click.press-this', function() {
732 var $this = $( this );
734 $this.toggleClass( 'is-toggled' );
735 $this.attr( 'aria-expanded', 'false' === $this.attr( 'aria-expanded' ) ? 'true' : 'false' );
736 $( '.setting-modal .add-category, .categories-search-wrapper' ).toggleClass( 'is-hidden' );
739 $( 'button.add-cat-submit' ).on( 'click.press-this', saveNewCategory );
741 $( '.categories-search' ).on( 'keyup.press-this', function() {
742 var search = $( this ).val().toLowerCase() || '';
744 // Don't search when less thasn 3 extended ASCII chars
745 if ( /[\x20-\xFF]+/.test( search ) && search.length < 2 ) {
749 $.each( catsCache, function( i, cat ) {
750 cat.node.removeClass( 'is-hidden searched-parent' );
754 $.each( catsCache, function( i, cat ) {
755 if ( cat.text.indexOf( search ) === -1 ) {
756 cat.node.addClass( 'is-hidden' );
758 cat.parents.addClass( 'searched-parent' );
767 function refreshCatsCache() {
768 $( '.categories-select' ).find( 'li' ).each( function() {
769 var $this = $( this );
773 parents: $this.parents( 'li' ),
774 text: $this.children( '.category' ).text().toLowerCase()
780 $( document ).ready( function() {
786 // Expose public methods?
788 renderNotice: renderNotice,
789 renderError: renderError
793 window.wp = window.wp || {};
794 window.wp.pressThis = new PressThis();
796 }( jQuery, window ));