5 ( function( $, window ) {
6 var PressThis = function() {
7 var editor, $mediaList, $mediaThumbWrap,
9 $document = $( document ),
11 textarea = document.createElement( 'textarea' ),
12 sidebarIsOpen = false,
13 settings = window.wpPressThisConfig || {},
14 data = window.wpPressThisData || {},
18 isOffScreen = 'is-off-screen',
19 isHidden = 'is-hidden',
20 offscreenHidden = isOffScreen + ' ' + isHidden,
21 iOS = /iPad|iPod|iPhone/.test( window.navigator.userAgent ),
22 $textEditor = $( '#pressthis' ),
23 textEditor = $textEditor[0],
24 textEditorMinHeight = 600,
26 transitionEndEvent = ( function() {
27 var style = document.documentElement.style;
29 if ( typeof style.transition !== 'undefined' ) {
30 return 'transitionend';
33 if ( typeof style.WebkitTransition !== 'undefined' ) {
34 return 'webkitTransitionEnd';
40 /* ***************************************************************
42 *************************************************************** */
45 * Emulates our PHP __() gettext function, powered by the strings exported in pressThisL10n.
47 * @param key string Key of the string to be translated, as found in pressThisL10n.
48 * @returns string Original or translated string, or empty string if no key.
51 if ( key && window.pressThisL10n ) {
52 return window.pressThisL10n[key] || key;
61 * @param string string Text to have the HTML tags striped out of.
62 * @returns string Stripped text.
64 function stripTags( string ) {
65 string = string || '';
68 .replace( /<!--[\s\S]*?(-->|$)/g, '' )
69 .replace( /<(script|style)[^>]*>[\s\S]*?(<\/\1>|$)/ig, '' )
70 .replace( /<\/?[a-z][\s\S]*?(>|$)/ig, '' );
74 * Strip HTML tags and convert HTML entities.
76 * @param text string Text.
77 * @returns string Sanitized text.
79 function sanitizeText( text ) {
80 var _text = stripTags( text );
83 textarea.innerHTML = _text;
84 _text = stripTags( textarea.value );
91 * Allow only HTTP or protocol relative URLs.
93 * @param url string The URL.
94 * @returns string Processed URL.
96 function checkUrl( url ) {
97 url = $.trim( url || '' );
99 if ( /^(?:https?:)?\/\//.test( url ) ) {
100 url = stripTags( url );
101 return url.replace( /["\\]+/g, '' );
110 function showSpinner() {
111 $( '.spinner' ).addClass( 'is-active' );
112 $( '.post-actions button' ).attr( 'disabled', 'disabled' );
118 function hideSpinner() {
119 $( '.spinner' ).removeClass( 'is-active' );
120 $( '.post-actions button' ).removeAttr( 'disabled' );
123 function textEditorResize( reset ) {
124 var pageYOffset, height;
126 if ( editor && ! editor.isHidden() ) {
130 reset = ( reset === 'reset' ) || ( textLength && textLength > textEditor.value.length );
131 height = textEditor.style.height;
134 pageYOffset = window.pageYOffset;
136 textEditor.style.height = 'auto';
137 textEditor.style.height = Math.max( textEditor.scrollHeight, textEditorMinHeight ) + 'px';
138 window.scrollTo( window.pageXOffset, pageYOffset );
139 } else if ( parseInt( textEditor.style.height, 10 ) < textEditor.scrollHeight ) {
140 textEditor.style.height = textEditor.scrollHeight + 'px';
143 textLength = textEditor.value.length;
146 function mceGetCursorOffset() {
151 var node = editor.selection.getNode(),
154 if ( editor.wp && editor.wp.getView && ( view = editor.wp.getView( node ) ) ) {
155 offset = view.getBoundingClientRect();
157 range = editor.selection.getRng();
160 offset = range.getClientRects()[0];
164 offset = node.getBoundingClientRect();
168 return offset.height ? offset : false;
171 // Make sure the caret is always visible.
172 function mceKeyup( event ) {
173 var VK = window.tinymce.util.VK,
176 // Bail on special keys.
177 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 ) ) {
179 // OS keys, function keys, num lock, scroll lock
180 } else if ( ( key >= 91 && key <= 93 ) || ( key >= 112 && key <= 123 ) || key === 144 || key === 145 ) {
187 function mceScroll( key ) {
188 var cursorTop, cursorBottom, editorBottom,
189 offset = mceGetCursorOffset(),
192 VK = window.tinymce.util.VK;
198 cursorTop = offset.top + editor.iframeElement.getBoundingClientRect().top;
199 cursorBottom = cursorTop + offset.height;
200 cursorTop = cursorTop - bufferTop;
201 cursorBottom = cursorBottom + bufferBottom;
202 editorBottom = $window.height();
204 // Don't scroll if the node is taller than the visible part of the editor
205 if ( editorBottom < offset.height ) {
209 if ( cursorTop < 0 && ( key === VK.UP || key === VK.LEFT || key === VK.BACKSPACE ) ) {
210 window.scrollTo( window.pageXOffset, cursorTop + window.pageYOffset );
211 } else if ( cursorBottom > editorBottom ) {
212 window.scrollTo( window.pageXOffset, cursorBottom + window.pageYOffset - editorBottom );
217 * Replace emoji images with chars and sanitize the text content.
219 function getTitleText() {
220 var $element = $( '#title-container' );
222 $element.find( 'img.emoji' ).each( function() {
223 var $image = $( this );
224 $image.replaceWith( $( '<span>' ).text( $image.attr( 'alt' ) ) );
227 return sanitizeText( $element.text() );
231 * Prepare the form data for saving.
233 function prepareFormData() {
234 var $form = $( '#pressthis-form' ),
235 $input = $( '<input type="hidden" name="post_category[]" value="">' );
237 editor && editor.save();
239 $( '#post_title' ).val( getTitleText() );
241 // Make sure to flush out the tags with tagBox before saving
242 if ( window.tagBox ) {
243 $( 'div.tagsdiv' ).each( function() {
244 window.tagBox.flushTags( this, false, 1 );
248 // Get selected categories
249 $( '.categories-select .category' ).each( function( i, element ) {
250 var $cat = $( element );
252 if ( $cat.hasClass( 'selected' ) ) {
253 // Have to append a node as we submit the actual form on preview
254 $form.append( $input.clone().val( $cat.attr( 'data-term-id' ) || '' ) );
260 * Submit the post form via AJAX, and redirect to the proper screen if published vs saved as a draft.
262 * @param action string publish|draft
264 function submitPost( action ) {
270 if ( 'publish' === action ) {
271 $( '#post_status' ).val( 'publish' );
275 data = $( '#pressthis-form' ).serialize();
281 }).always( function() {
284 $( '.publish-button' ).removeClass( 'is-saving' );
285 }).done( function( response ) {
286 if ( ! response.success ) {
287 renderError( response.data.errorMessage );
288 } else if ( response.data.redirect ) {
289 if ( window.opener && ( settings.redirInParent || response.data.force ) ) {
291 window.opener.location.href = response.data.redirect;
293 window.setTimeout( function() {
297 window.location.href = response.data.redirect;
300 window.location.href = response.data.redirect;
303 }).fail( function() {
304 renderError( __( 'serverError' ) );
309 * Inserts the media a user has selected from the presented list inside the editor, as an image or embed, based on type
311 * @param type string img|embed
312 * @param src string Source URL
313 * @param link string Optional destination link, for images (defaults to src)
315 function insertSelectedMedia( $element ) {
316 var src, link, newContent = '';
318 src = checkUrl( $element.attr( 'data-wp-src' ) || '' );
319 link = checkUrl( data.u );
321 if ( $element.hasClass( 'is-image' ) ) {
326 newContent = '<a href="' + link + '"><img class="alignnone size-full" src="' + src + '" alt="" /></a>';
328 newContent = '[embed]' + src + '[/embed]';
331 if ( editor && ! editor.isHidden() ) {
332 if ( ! hasSetFocus ) {
333 editor.setContent( '<p>' + newContent + '</p>' + editor.getContent() );
335 editor.execCommand( 'mceInsertContent', false, newContent );
337 } else if ( window.QTags ) {
338 window.QTags.insertContent( newContent );
343 * Save a new user-generated category via AJAX
345 function saveNewCategory() {
347 name = $( '#new-category' ).val();
354 action: 'press-this-add-category',
355 post_id: $( '#post_ID' ).val() || 0,
357 new_cat_nonce: $( '#_ajax_nonce-add-category' ).val() || '',
358 parent: $( '#new-category-parent' ).val() || 0
361 $.post( window.ajaxurl, data, function( response ) {
362 if ( ! response.success ) {
363 renderError( response.data.errorMessage );
366 $wrap = $( 'ul.categories-select' );
368 $.each( response.data, function( i, newCat ) {
369 var $node = $( '<li>' ).append( $( '<div class="category selected" tabindex="0" role="checkbox" aria-checked="true">' )
370 .attr( 'data-term-id', newCat.term_id )
371 .text( newCat.name ) );
373 if ( newCat.parent ) {
374 if ( ! $ul || ! $ul.length ) {
375 $parent = $wrap.find( 'div[data-term-id="' + newCat.parent + '"]' ).parent();
376 $ul = $parent.find( 'ul.children:first' );
378 if ( ! $ul.length ) {
379 $ul = $( '<ul class="children">' ).appendTo( $parent );
383 $ul.prepend( $node );
385 $wrap.prepend( $node );
396 /* ***************************************************************
397 * RENDERING FUNCTIONS
398 *************************************************************** */
401 * Hide the form letting users enter a URL to be scanned, if a URL was already passed.
403 function renderToolsVisibility() {
404 if ( data.hasData ) {
405 $( '#scanbar' ).hide();
410 * Render error notice
412 * @param msg string Notice/error message
413 * @param error string error|notice CSS class for display
415 function renderNotice( msg, error ) {
416 var $alerts = $( '.editor-wrapper div.alerts' ),
417 className = error ? 'is-error' : 'is-notice';
419 $alerts.append( $( '<p class="alert ' + className + '">' ).text( msg ) );
423 * Render error notice
425 * @param msg string Error message
427 function renderError( msg ) {
428 renderNotice( msg, true );
431 function clearNotices() {
432 $( 'div.alerts' ).empty();
436 * Render notices on page load, if any already
438 function renderStartupNotices() {
439 // Render errors sent in the data, if any
441 $.each( data.errors, function( i, msg ) {
448 * Add an image to the list of found images.
450 function addImg( src, displaySrc, i ) {
451 var $element = $mediaThumbWrap.clone().addClass( 'is-image' );
453 $element.attr( 'data-wp-src', src ).css( 'background-image', 'url(' + displaySrc + ')' )
454 .find( 'span' ).text( __( 'suggestedImgAlt' ).replace( '%d', i + 1 ) );
456 $mediaList.append( $element );
460 * Render the detected images and embed for selection, if any
462 function renderDetectedMedia() {
465 $mediaList = $( 'ul.media-list' );
466 $mediaThumbWrap = $( '<li class="suggested-media-thumbnail" tabindex="0"><span class="screen-reader-text"></span></li>' );
468 if ( data._embeds ) {
469 $.each( data._embeds, function ( i, src ) {
472 $element = $mediaThumbWrap.clone().addClass( 'is-embed' );
474 src = checkUrl( src );
476 if ( src.indexOf( 'youtube.com/' ) > -1 ) {
477 displaySrc = 'https://i.ytimg.com/vi/' + src.replace( /.+v=([^&]+).*/, '$1' ) + '/hqdefault.jpg';
478 cssClass += ' is-video';
479 } else if ( src.indexOf( 'youtu.be/' ) > -1 ) {
480 displaySrc = 'https://i.ytimg.com/vi/' + src.replace( /\/([^\/])$/, '$1' ) + '/hqdefault.jpg';
481 cssClass += ' is-video';
482 } else if ( src.indexOf( 'dailymotion.com' ) > -1 ) {
483 displaySrc = src.replace( '/video/', '/thumbnail/video/' );
484 cssClass += ' is-video';
485 } else if ( src.indexOf( 'soundcloud.com' ) > -1 ) {
486 cssClass += ' is-audio';
487 } else if ( src.indexOf( 'twitter.com' ) > -1 ) {
488 cssClass += ' is-tweet';
490 cssClass += ' is-video';
493 $element.attr( 'data-wp-src', src ).find( 'span' ).text( __( 'suggestedEmbedAlt' ).replace( '%d', i + 1 ) );
496 $element.css( 'background-image', 'url(' + displaySrc + ')' );
499 $mediaList.append( $element );
504 if ( data._images ) {
505 $.each( data._images, function( i, src ) {
506 var displaySrc, img = new Image();
508 src = checkUrl( src );
509 displaySrc = src.replace( /^(http[^\?]+)(\?.*)?$/, '$1' );
511 if ( src.indexOf( 'files.wordpress.com/' ) > -1 ) {
512 displaySrc = displaySrc.replace( /\?.*$/, '' ) + '?w=' + smallestWidth;
513 } else if ( src.indexOf( 'gravatar.com/' ) > -1 ) {
514 displaySrc = displaySrc.replace( /\?.*$/, '' ) + '?s=' + smallestWidth;
519 img.onload = function() {
520 if ( ( img.width && img.width < 256 ) ||
521 ( img.height && img.height < 128 ) ) {
526 addImg( src, displaySrc, i );
535 $( '.media-list-container' ).addClass( 'has-media' );
539 /* ***************************************************************
540 * MONITORING FUNCTIONS
541 *************************************************************** */
544 * Interactive navigation behavior for the options modal (post format, tags, categories)
546 function monitorOptionsModal() {
547 var $postOptions = $( '.post-options' ),
548 $postOption = $( '.post-option' ),
549 $settingModal = $( '.setting-modal' ),
550 $modalClose = $( '.modal-close' );
552 $postOption.on( 'click', function() {
553 var index = $( this ).index(),
554 $targetSettingModal = $settingModal.eq( index );
556 $postOptions.addClass( isOffScreen )
557 .one( transitionEndEvent, function() {
558 $( this ).addClass( isHidden );
561 $targetSettingModal.removeClass( offscreenHidden )
562 .one( transitionEndEvent, function() {
563 $( this ).find( '.modal-close' ).focus();
567 $modalClose.on( 'click', function() {
568 var $targetSettingModal = $( this ).parent(),
569 index = $targetSettingModal.index();
571 $postOptions.removeClass( offscreenHidden );
572 $targetSettingModal.addClass( isOffScreen );
574 if ( transitionEndEvent ) {
575 $targetSettingModal.one( transitionEndEvent, function() {
576 $( this ).addClass( isHidden );
577 $postOption.eq( index - 1 ).focus();
580 setTimeout( function() {
581 $targetSettingModal.addClass( isHidden );
582 $postOption.eq( index - 1 ).focus();
589 * Interactive behavior for the sidebar toggle, to show the options modals
591 function openSidebar() {
592 sidebarIsOpen = true;
594 $( '.options' ).removeClass( 'closed' ).addClass( 'open' );
595 $( '.press-this-actions, #scanbar' ).addClass( isHidden );
596 $( '.options-panel-back' ).removeClass( isHidden );
598 $( '.options-panel' ).removeClass( offscreenHidden )
599 .one( transitionEndEvent, function() {
600 $( '.post-option:first' ).focus();
604 function closeSidebar() {
605 sidebarIsOpen = false;
607 $( '.options' ).removeClass( 'open' ).addClass( 'closed' );
608 $( '.options-panel-back' ).addClass( isHidden );
609 $( '.press-this-actions, #scanbar' ).removeClass( isHidden );
611 $( '.options-panel' ).addClass( isOffScreen )
612 .one( transitionEndEvent, function() {
613 $( this ).addClass( isHidden );
614 // Reset to options list
615 $( '.post-options' ).removeClass( offscreenHidden );
616 $( '.setting-modal').addClass( offscreenHidden );
621 * Interactive behavior for the post title's field placeholder
623 function monitorPlaceholder() {
624 var $titleField = $( '#title-container' ),
625 $placeholder = $( '.post-title-placeholder' );
627 $titleField.on( 'focus', function() {
628 $placeholder.addClass( 'is-hidden' );
629 }).on( 'blur', function() {
630 if ( ! $titleField.text() && ! $titleField.html() ) {
631 $placeholder.removeClass( 'is-hidden' );
633 }).on( 'keyup', function() {
635 }).on( 'paste', function( event ) {
637 clipboard = event.originalEvent.clipboardData || window.clipboardData;
641 text = clipboard.getData( 'Text' ) || clipboard.getData( 'text/plain' );
644 text = $.trim( text.replace( /\s+/g, ' ' ) );
646 if ( window.getSelection ) {
647 range = window.getSelection().getRangeAt(0);
650 if ( ! range.collapsed ) {
651 range.deleteContents();
654 range.insertNode( document.createTextNode( text ) );
656 } else if ( document.selection ) {
657 range = document.selection.createRange();
666 event.preventDefault();
671 setTimeout( function() {
672 $titleField.text( getTitleText() );
676 if ( $titleField.text() || $titleField.html() ) {
677 $placeholder.addClass('is-hidden');
681 function toggleCatItem( $element ) {
682 if ( $element.hasClass( 'selected' ) ) {
683 $element.removeClass( 'selected' ).attr( 'aria-checked', 'false' );
685 $element.addClass( 'selected' ).attr( 'aria-checked', 'true' );
689 function monitorCatList() {
690 $( '.categories-select' ).on( 'click.press-this keydown.press-this', function( event ) {
691 var $element = $( event.target );
693 if ( $element.is( 'div.category' ) ) {
694 if ( event.type === 'keydown' && event.keyCode !== 32 ) {
698 toggleCatItem( $element );
699 event.preventDefault();
704 function splitButtonClose() {
705 $( '.split-button' ).removeClass( 'is-open' );
706 $( '.split-button-toggle' ).attr( 'aria-expanded', 'false' );
709 /* ***************************************************************
710 * PROCESSING FUNCTIONS
711 *************************************************************** */
714 * Calls all the rendring related functions to happen on page load
718 renderToolsVisibility();
719 renderDetectedMedia();
720 renderStartupNotices();
722 if ( window.tagBox ) {
723 window.tagBox.init();
726 // iOS doesn't fire click events on "standard" elements without this...
728 $( document.body ).css( 'cursor', 'pointer' );
733 * Set app events and other state monitoring related code.
736 var $splitButton = $( '.split-button' );
738 $document.on( 'tinymce-editor-init', function( event, ed ) {
741 editor.on( 'nodechange', function() {
745 editor.on( 'focus', function() {
749 editor.on( 'show', function() {
750 setTimeout( function() {
751 editor.execCommand( 'wpAutoResize' );
755 editor.on( 'hide', function() {
756 setTimeout( function() {
757 textEditorResize( 'reset' );
761 editor.on( 'keyup', mceKeyup );
762 editor.on( 'undo redo', mceScroll );
764 }).on( 'click.press-this keypress.press-this', '.suggested-media-thumbnail', function( event ) {
765 if ( event.type === 'click' || event.keyCode === 13 ) {
766 insertSelectedMedia( $( this ) );
768 }).on( 'click.press-this', function( event ) {
769 if ( ! $( event.target ).closest( 'button' ).hasClass( 'split-button-toggle' ) ) {
774 // Publish, Draft and Preview buttons
775 $( '.post-actions' ).on( 'click.press-this', function( event ) {
777 $target = $( event.target ),
778 $button = $target.closest( 'button' );
780 if ( $button.length ) {
781 if ( $button.hasClass( 'draft-button' ) ) {
782 $( '.publish-button' ).addClass( 'is-saving' );
783 submitPost( 'draft' );
784 } else if ( $button.hasClass( 'publish-button' ) ) {
785 $button.addClass( 'is-saving' );
787 if ( window.history.replaceState ) {
788 location = window.location.href;
789 location += ( location.indexOf( '?' ) !== -1 ) ? '&' : '?';
790 location += 'wp-press-this-reload=true';
792 window.history.replaceState( null, null, location );
795 submitPost( 'publish' );
796 } else if ( $button.hasClass( 'preview-button' ) ) {
798 window.opener && window.opener.focus();
800 $( '#wp-preview' ).val( 'dopreview' );
801 $( '#pressthis-form' ).attr( 'target', '_blank' ).submit().attr( 'target', '' );
802 $( '#wp-preview' ).val( '' );
803 } else if ( $button.hasClass( 'standard-editor-button' ) ) {
804 $( '.publish-button' ).addClass( 'is-saving' );
805 $( '#pt-force-redirect' ).val( 'true' );
806 submitPost( 'draft' );
807 } else if ( $button.hasClass( 'split-button-toggle' ) ) {
808 if ( $splitButton.hasClass( 'is-open' ) ) {
809 $splitButton.removeClass( 'is-open' );
810 $button.attr( 'aria-expanded', 'false' );
812 $splitButton.addClass( 'is-open' );
813 $button.attr( 'aria-expanded', 'true' );
819 monitorOptionsModal();
820 monitorPlaceholder();
823 $( '.options' ).on( 'click.press-this', function() {
824 if ( $( this ).hasClass( 'open' ) ) {
831 // Close the sidebar when focus moves outside of it.
832 $( '.options-panel, .options-panel-back' ).on( 'focusout.press-this', function() {
833 setTimeout( function() {
834 var node = document.activeElement,
837 if ( sidebarIsOpen && node && ! $node.hasClass( 'options-panel-back' ) &&
838 ( node.nodeName === 'BODY' ||
839 ( ! $node.closest( '.options-panel' ).length &&
840 ! $node.closest( '.options' ).length ) ) ) {
847 $( '#post-formats-select input' ).on( 'change', function() {
848 var $this = $( this );
850 if ( $this.is( ':checked' ) ) {
851 $( '#post-option-post-format' ).text( $( 'label[for="' + $this.attr( 'id' ) + '"]' ).text() || '' );
855 $window.on( 'beforeunload.press-this', function() {
856 if ( saveAlert || ( editor && editor.isDirty() ) ) {
857 return __( 'saveAlert' );
859 } ).on( 'resize.press-this', function() {
860 if ( ! editor || editor.isHidden() ) {
861 textEditorResize( 'reset' );
865 $( 'button.add-cat-toggle' ).on( 'click.press-this', function() {
866 var $this = $( this );
868 $this.toggleClass( 'is-toggled' );
869 $this.attr( 'aria-expanded', 'false' === $this.attr( 'aria-expanded' ) ? 'true' : 'false' );
870 $( '.setting-modal .add-category, .categories-search-wrapper' ).toggleClass( 'is-hidden' );
873 $( 'button.add-cat-submit' ).on( 'click.press-this', saveNewCategory );
875 $( '.categories-search' ).on( 'keyup.press-this', function() {
876 var search = $( this ).val().toLowerCase() || '';
878 // Don't search when less thasn 3 extended ASCII chars
879 if ( /[\x20-\xFF]+/.test( search ) && search.length < 2 ) {
883 $.each( catsCache, function( i, cat ) {
884 cat.node.removeClass( 'is-hidden searched-parent' );
888 $.each( catsCache, function( i, cat ) {
889 if ( cat.text.indexOf( search ) === -1 ) {
890 cat.node.addClass( 'is-hidden' );
892 cat.parents.addClass( 'searched-parent' );
898 $textEditor.on( 'focus.press-this input.press-this propertychange.press-this', textEditorResize );
903 function refreshCatsCache() {
904 $( '.categories-select' ).find( 'li' ).each( function() {
905 var $this = $( this );
909 parents: $this.parents( 'li' ),
910 text: $this.children( '.category' ).text().toLowerCase()
916 $document.ready( function() {
922 // Expose public methods?
924 renderNotice: renderNotice,
925 renderError: renderError
929 window.wp = window.wp || {};
930 window.wp.pressThis = new PressThis();
932 }( jQuery, window ));