/** * PressThis App * */ ( function( $, window ) { var PressThis = function() { var editor, saveAlert = false, siteConfig = window.wpPressThisConfig || {}, data = window.wpPressThisData || {}, smallestWidth = 128, interestingImages = getInterestingImages( data ) || [], interestingEmbeds = getInterestingEmbeds( data ) || [], hasEmptyTitleStr = false, suggestedTitleStr = getSuggestedTitle( data ), suggestedContentStr = getSuggestedContent( data ), hasSetFocus = false, catsCache = [], 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 || ''; } /** * Strips HTML tags * * @param string string Text to have the HTML tags striped out of. * @returns string Stripped text. */ function stripTags( string ) { string = string || ''; return string .replace( /|$)/g, '' ) .replace( /<(script|style)[^>]*>[\s\S]*?(<\/\1>|$)/ig, '' ) .replace( /<\/?[a-z][^>]*>/ig, '' ); } /** * Strip HTML tags and entity encode some of the HTML special chars. * * @param text string Text. * @returns string Sanitized text. */ function sanitizeText( text ) { text = stripTags( text ); return text .replace( /\\/, '' ) .replace( //g, '>' ) .replace( /"/g, '"' ) .replace( /'/g, ''' ); } /** * 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 = stripTags( url ); return url.replace( /["\\]+/g, '' ); } return ''; } /** * Gets the source page's canonical link, based on passed location and meta data. * * @param data object Usually WpPressThis_App.data * @returns string Discovered canonical URL, or empty */ function getCanonicalLink( data ) { if ( ! data || data.length ) { return ''; } var link = ''; if ( data._links ) { if ( data._links.canonical && data._links.canonical.length ) { link = data._links.canonical; } } if ( ! link.length && data.u ) { link = data.u; } if ( ! link.length && data._meta ) { if ( data._meta['twitter:url'] && data._meta['twitter:url'].length ) { link = data._meta['twitter:url']; } else if ( data._meta['og:url'] && data._meta['og:url'].length ) { link = data._meta['og:url']; } } return decodeURI( link ); } /** * Gets the source page's site name, based on passed meta data. * * @param data object Usually WpPressThis_App.data * @returns string Discovered site name, or empty */ function getSourceSiteName( data ) { if ( ! data || data.length ) { return ''; } var name=''; if ( data._meta ) { if ( data._meta['og:site_name'] && data._meta['og:site_name'].length ) { name = data._meta['og:site_name']; } else if ( data._meta['application-name'] && data._meta['application-name'].length ) { name = data._meta['application-name']; } } return name.replace( /\\/g, '' ); } /** * Gets the source page's title, based on passed title and meta data. * * @param data object Usually WpPressThis_App.data * @returns string Discovered page title, or empty */ function getSuggestedTitle( data ) { if ( ! data || data.length ) { return __( 'newPost' ); } var title = ''; if ( data.t ) { title = data.t; } if ( ! title.length && data._meta ) { if ( data._meta['twitter:title'] && data._meta['twitter:title'].length ) { title = data._meta['twitter:title']; } else if ( data._meta['og:title'] && data._meta['og:title'].length ) { title = data._meta['og:title']; } else if ( data._meta.title && data._meta.title.length ) { title = data._meta.title; } } if ( ! title.length ) { title = __( 'newPost' ); hasEmptyTitleStr = true; } return title.replace( /\\/g, '' ); } /** * Gets the source page's suggested content, based on passed data (description, selection, etc). * Features a blockquoted excerpt, as well as content attribution, if any. * * @param data object Usually WpPressThis_App.data * @returns string Discovered content, or empty */ function getSuggestedContent( data ) { if ( ! data || data.length ) { return ''; } var content = '', title = getSuggestedTitle( data ), url = getCanonicalLink( data ), siteName = getSourceSiteName( data ); if ( data.s && data.s.length ) { content = data.s; } else if ( data._meta ) { if ( data._meta['twitter:description'] && data._meta['twitter:description'].length ) { content = data._meta['twitter:description']; } else if ( data._meta['og:description'] && data._meta['og:description'].length ) { content = data._meta['og:description']; } else if ( data._meta.description && data._meta.description.length ) { content = data._meta.description; } } // Wrap suggested content in blockquote tag, if we have any. content = ( content.length ? '
' + sanitizeText( content ) + '
' : '' ); // Add a source attribution if there is one available. if ( ( ( title.length && __( 'newPost' ) !== title ) || siteName.length ) && url.length ) { content += '

'; content += __( 'source' ); content += ' '; content += __( 'sourceLink').replace( '%1$s', encodeURI( url ) ).replace( '%2$s', sanitizeText( title || siteName ) ); content += '

'; } if ( ! content ) { content = ''; } return content.replace( /\\/g, '' ); } /** * Tests if what was passed as an embed URL is deemed to be embeddable in the editor. * * @param url string Passed URl, usually from WpPressThis_App.data._embed * @returns boolean */ function isEmbeddable( url ) { if ( ! url ) { return false; } else if ( url.match( /\/\/(m\.|www\.)?youtube\.com\/watch\?/ ) || url.match( /\/youtu\.be\/.+$/ ) ) { return true; } else if ( url.match( /\/\/vimeo\.com\/(.+\/)?[\d]+$/ ) ) { return true; } else if ( url.match( /\/\/(www\.)?dailymotion\.com\/video\/.+$/ ) ) { return true; } else if ( url.match( /\/\/soundcloud\.com\/.+$/ ) ) { return true; } else if ( url.match( /\/\/twitter\.com\/[^\/]+\/status\/[\d]+$/ ) ) { return true; } else if ( url.match( /\/\/vine\.co\/v\/[^\/]+/ ) ) { return true; } return false; } /** * Tests if what was passed as an image URL is deemed to be interesting enough to offer to the user for selection. * * @param src string Passed URl, usually from WpPressThis_App.data._ing * @returns boolean Test for false */ function isSrcUninterestingPath( src ) { if ( src.match( /\/ad[sx]{1}?\// ) ) { // Ads return true; } else if ( src.match( /(\/share-?this[^\.]+?\.[a-z0-9]{3,4})(\?.*)?$/ ) ) { // Share-this type button return true; } else if ( src.match( /\/(spinner|loading|spacer|blank|rss)\.(gif|jpg|png)/ ) ) { // Loaders, spinners, spacers return true; } else if ( src.match( /\/([^\.\/]+[-_]{1})?(spinner|loading|spacer|blank)s?([-_]{1}[^\.\/]+)?\.[a-z0-9]{3,4}/ ) ) { // Fancy loaders, spinners, spacers return true; } else if ( src.match( /([^\.\/]+[-_]{1})?thumb[^.]*\.(gif|jpg|png)$/ ) ) { // Thumbnails, too small, usually irrelevant to context return true; } else if ( src.match( /\/wp-includes\// ) ) { // Classic WP interface images return true; } else if ( src.match( /[^\d]{1}\d{1,2}x\d+\.(gif|jpg|png)$/ ) ) { // Most often tiny buttons/thumbs (< 100px wide) return true; } else if ( src.indexOf( '/g.gif' ) > -1 ) { // Classic WP stats gif return true; } else if ( src.indexOf( '/pixel.mathtag.com' ) > -1 ) { // See mathtag.com return true; } return false; } /** * Get a list of valid embeds from what was passed via WpPressThis_App.data._embed on page load. * * @returns array */ function getInterestingEmbeds() { var embeds = data._embed || [], interestingEmbeds = [], alreadySelected = []; if ( embeds.length ) { $.each( embeds, function ( i, src ) { if ( !src || !src.length ) { // Skip: no src value return; } else if ( !isEmbeddable( src ) ) { // Skip: not deemed embeddable return; } var schemelessSrc = src.replace( /^https?:/, '' ); if ( $.inArray( schemelessSrc, alreadySelected ) > -1 ) { // Skip: already shown return; } interestingEmbeds.push( src ); alreadySelected.push( schemelessSrc ); } ); } return interestingEmbeds; } /** * Get what is likely the most valuable image from what was passed via WpPressThis_App.data._img and WpPressThis_App.data._meta on page load. * * @returns array */ function getFeaturedImage( data ) { var featured = ''; if ( ! data || ! data._meta ) { return ''; } if ( data._meta['twitter:image0:src'] && data._meta['twitter:image0:src'].length ) { featured = data._meta['twitter:image0:src']; } else if ( data._meta['twitter:image0'] && data._meta['twitter:image0'].length ) { featured = data._meta['twitter:image0']; } else if ( data._meta['twitter:image:src'] && data._meta['twitter:image:src'].length ) { featured = data._meta['twitter:image:src']; } else if ( data._meta['twitter:image'] && data._meta['twitter:image'].length ) { featured = data._meta['twitter:image']; } else if ( data._meta['og:image'] && data._meta['og:image'].length ) { featured = data._meta['og:image']; } else if ( data._meta['og:image:secure_url'] && data._meta['og:image:secure_url'].length ) { featured = data._meta['og:image:secure_url']; } featured = checkUrl( featured ); return ( isSrcUninterestingPath( featured ) ) ? '' : featured; } /** * Get a list of valid images from what was passed via WpPressThis_App.data._img and WpPressThis_App.data._meta on page load. * * @returns array */ function getInterestingImages( data ) { var imgs = data._img || [], featuredPict = getFeaturedImage( data ) || '', interestingImgs = [], alreadySelected = []; if ( featuredPict.length ) { interestingImgs.push( featuredPict ); alreadySelected.push( featuredPict.replace(/^https?:/, '') ); } if ( imgs.length ) { $.each( imgs, function ( i, src ) { src = src.replace( /http:\/\/[\d]+\.gravatar\.com\//, 'https://secure.gravatar.com/' ); src = checkUrl( src ); if ( ! src || ! src.length ) { // Skip: no src value return; } var schemelessSrc = src.replace( /^https?:/, '' ); if ( Array.prototype.indexOf && alreadySelected.indexOf( schemelessSrc ) > -1 ) { // Skip: already shown return; } else if ( isSrcUninterestingPath( src ) ) { // Skip: spinner, stat, ad, or spacer pict return; } else if ( src.indexOf( 'avatar' ) > -1 && interestingImgs.length >= 15 ) { // Skip: some type of avatar and we've already gathered more than 23 diff images to show return; } interestingImgs.push( src ); alreadySelected.push( schemelessSrc ); } ); } return interestingImgs; } /** * Show UX spinner */ function showSpinner() { $( '#spinner' ).addClass( 'show' ); $( '.post-actions button' ).each( function() { $( this ).attr( 'disabled', 'disabled' ); } ); } /** * Hide UX spinner */ function hideSpinner() { $( '#spinner' ).removeClass( 'show' ); $( '.post-actions button' ).each( function() { $( this ).removeAttr( 'disabled' ); } ); } /** * 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 ) { saveAlert = false; showSpinner(); var $form = $( '#pressthis-form' ); if ( 'publish' === action ) { $( '#post_status' ).val( 'publish' ); } editor && editor.save(); $( '#title-field' ).val( sanitizeText( $( '#title-container' ).text() ) ); // Make sure to flush out the tags with tagBox before saving if ( window.tagBox ) { $( 'div.tagsdiv' ).each( function() { window.tagBox.flushTags( this, false, 1 ); } ); } var data = $form.serialize(); $.ajax( { type: 'post', url: window.ajaxurl, data: data, success: function( response ) { if ( ! response.success ) { renderError( response.data.errorMessage ); hideSpinner(); } else if ( response.data.redirect ) { if ( window.opener && siteConfig.redir_in_parent ) { try { window.opener.location.href = response.data.redirect; } catch( er ) {} window.self.close(); } else { window.location.href = response.data.redirect; } } } } ); } /** * 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( type, src, link ) { var newContent = ''; if ( ! editor ) { return; } src = checkUrl( src ); link = checkUrl( link ); if ( 'img' === type ) { if ( ! link || ! link.length ) { link = src; } newContent = '\n'; } else { newContent = '[embed]' + src + '[/embed]\n'; } if ( ! hasSetFocus ) { // Append to top of content on 1st media insert editor.setContent( newContent + editor.getContent() ); } else { // Or add where the cursor was last positioned in TinyMCE editor.execCommand( 'mceInsertContent', false, newContent ); } hasSetFocus = true; } /** * Adds the currently selected post format next to the option, in the options panel. * * @param format string Post format to be displayed */ function setPostFormatString( format ) { if ( ! format || ! siteConfig || ! siteConfig.post_formats || ! siteConfig.post_formats[ format ] ) { return; } $( '#post-option-post-format' ).text( siteConfig.post_formats[ format ] ); } /** * 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 { // TODO: change if/when the html changes. var $parent, $ul, $wrap = $( 'ul.categories-select' ); $.each( response.data, function( i, newCat ) { var $node = $( '
  • ' ).attr( 'id', 'category-' + newCat.term_id ) .append( $( '