├── js ├── dummy.js ├── admin-widgets.js ├── counter.min.js ├── admin-post.js ├── counter.js ├── frontend.min.js ├── admin-quick-edit.js ├── admin-settings.js ├── frontend.js ├── block-editor.min.js ├── column-modal.js └── admin-dashboard.js ├── index.php ├── .gitignore ├── css ├── page-reports.png ├── block-editor.min.css ├── frontend.min.css ├── frontend.css ├── admin-dashboard.min.css ├── admin.min.css ├── admin-dashboard.css ├── column-modal.css └── admin.css ├── .babelrc ├── blocks ├── post-views │ ├── src │ │ ├── index.js │ │ ├── block.json │ │ └── edit.js │ ├── build │ │ ├── index.asset.php │ │ ├── block.json │ │ └── index.js │ └── package.json └── most-viewed-posts │ ├── src │ ├── index.js │ ├── block.json │ └── edit.js │ ├── build │ ├── index.asset.php │ ├── block.json │ └── index.js │ └── package.json ├── src ├── block-editor.scss ├── frontend.scss ├── admin.scss ├── admin-dashboard.scss └── block-editor.js ├── .vscode └── settings.json ├── package.json ├── webpack.config.js ├── README.md ├── includes ├── class-cron.php ├── class-functions.php ├── class-toolbar.php ├── class-admin.php ├── class-frontend.php ├── class-update.php ├── class-widgets.php └── class-query.php ├── assets ├── microtip │ └── microtip.min.css └── micromodal │ └── micromodal.min.js └── readme.txt /js/dummy.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | blocks/most-viewed-posts/node_modules/ -------------------------------------------------------------------------------- /css/page-reports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfactoryplugins/post-views-counter/HEAD/css/page-reports.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | ["transform-react-jsx", { 5 | "pragma": "wp.element.createElement" 6 | } 7 | ] 8 | ] 9 | } -------------------------------------------------------------------------------- /blocks/post-views/src/index.js: -------------------------------------------------------------------------------- 1 | import { registerBlockType } from '@wordpress/blocks'; 2 | import Edit from './edit'; 3 | 4 | registerBlockType( "post-views-counter/post-views", { 5 | edit: Edit 6 | } ); -------------------------------------------------------------------------------- /blocks/most-viewed-posts/src/index.js: -------------------------------------------------------------------------------- 1 | import { registerBlockType } from '@wordpress/blocks'; 2 | import Edit from './edit'; 3 | 4 | registerBlockType( "post-views-counter/most-viewed-posts", { 5 | edit: Edit 6 | } ); -------------------------------------------------------------------------------- /blocks/post-views/build/index.asset.php: -------------------------------------------------------------------------------- 1 | array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-i18n', 'wp-server-side-render'), 'version' => '598d4d812518f2040464'); 2 | -------------------------------------------------------------------------------- /blocks/most-viewed-posts/build/index.asset.php: -------------------------------------------------------------------------------- 1 | array('react', 'react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-i18n', 'wp-server-side-render'), 'version' => '68d0dd1a77c84d6df79f'); 2 | -------------------------------------------------------------------------------- /css/block-editor.min.css: -------------------------------------------------------------------------------- 1 | .edit-post-post-views{gap:8px;margin-top:0}.edit-post-post-views-popover .components-popover__content{padding:10px;min-width:260px}.edit-post-post-views-popover .components-popover__content legend{font-weight:600;margin-bottom:1em;margin-top:.5em;padding:0} 2 | -------------------------------------------------------------------------------- /js/admin-widgets.js: -------------------------------------------------------------------------------- 1 | ( function( $ ) { 2 | 3 | // ready event 4 | $( function() { 5 | $( document ).on( 'change', '.pvc-show-post-thumbnail', function() { 6 | $( this ).closest( '.widget-content' ).find( '.pvc-post-thumbnail-size' ).fadeToggle( 300 ); 7 | } ); 8 | } ); 9 | 10 | } )( jQuery ); -------------------------------------------------------------------------------- /src/block-editor.scss: -------------------------------------------------------------------------------- 1 | .edit-post-post-views { 2 | gap: 8px; 3 | margin-top: 0; 4 | } 5 | .edit-post-post-views-popover { 6 | .components-popover__content { 7 | padding: 10px; 8 | min-width: 260px; 9 | 10 | legend { 11 | font-weight: 600; 12 | margin-bottom: 1em; 13 | margin-top: 0.5em; 14 | padding: 0; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/frontend.scss: -------------------------------------------------------------------------------- 1 | .post-views.entry-meta { 2 | > span { 3 | margin-right: 0 !important; 4 | font: 16px/1; 5 | 6 | &.post-views-icon.dashicons { 7 | display: inline-block; 8 | font-size: 16px; 9 | line-height: 1; 10 | text-decoration: inherit; 11 | vertical-align: middle; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": true, 3 | "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": true, 4 | "javascript.format.insertSpaceBeforeFunctionParenthesis": true, 5 | "javascript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": false, 6 | "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": true 7 | } -------------------------------------------------------------------------------- /blocks/post-views/src/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.wp.org/trunk/block.json", 3 | "apiVersion": 3, 4 | "name": "post-views-counter/post-views", 5 | "version": "1.0.0", 6 | "title": "Post Views", 7 | "category": "post-views-counter", 8 | "icon": "list-view", 9 | "description": "Display post views for a given post.", 10 | "example": {}, 11 | "attributes": { 12 | "postID": { 13 | "type": "integer", 14 | "default": 0 15 | }, 16 | "period": { 17 | "type": "string", 18 | "default": "total" 19 | } 20 | }, 21 | "supports": { 22 | "html": false 23 | }, 24 | "textdomain": "post-views-counter", 25 | "editorScript": "file:./index.js" 26 | } 27 | -------------------------------------------------------------------------------- /blocks/post-views/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "post-views-counter-post-views", 3 | "version": "1.0.0", 4 | "description": "Display post views for a given post.", 5 | "author": "Digital Factory", 6 | "license": "GPL-2.0-or-later", 7 | "main": "build/index.js", 8 | "scripts": { 9 | "build": "wp-scripts build --webpack-copy-php", 10 | "format": "wp-scripts format", 11 | "packages-update": "wp-scripts packages-update", 12 | "plugin-zip": "wp-scripts plugin-zip", 13 | "start": "wp-scripts start --webpack-copy-php" 14 | }, 15 | "devDependencies": { 16 | "@wordpress/scripts": "^30.4.0" 17 | }, 18 | "dependencies": { 19 | "@wordpress/server-side-render": "^5.11.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /blocks/most-viewed-posts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "post-views-counter-most-viewed-posts", 3 | "version": "1.0.0", 4 | "description": "Displays a list of most viewed posts.", 5 | "author": "Digital Factory", 6 | "license": "GPL-2.0-or-later", 7 | "main": "build/index.js", 8 | "scripts": { 9 | "build": "wp-scripts build --webpack-copy-php", 10 | "format": "wp-scripts format", 11 | "packages-update": "wp-scripts packages-update", 12 | "plugin-zip": "wp-scripts plugin-zip", 13 | "start": "wp-scripts start --webpack-copy-php" 14 | }, 15 | "devDependencies": { 16 | "@wordpress/scripts": "^30.4.0" 17 | }, 18 | "dependencies": { 19 | "@wordpress/server-side-render": "^5.11.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /blocks/post-views/build/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.wp.org/trunk/block.json", 3 | "apiVersion": 3, 4 | "name": "post-views-counter/post-views", 5 | "version": "1.0.0", 6 | "title": "Post Views", 7 | "category": "post-views-counter", 8 | "icon": "list-view", 9 | "description": "Display post views for a given post.", 10 | "example": {}, 11 | "attributes": { 12 | "postID": { 13 | "type": "integer", 14 | "default": 0 15 | }, 16 | "period": { 17 | "type": "string", 18 | "default": "total" 19 | } 20 | }, 21 | "supports": { 22 | "html": false 23 | }, 24 | "textdomain": "post-views-counter", 25 | "editorScript": "file:./index.js" 26 | } -------------------------------------------------------------------------------- /js/counter.min.js: -------------------------------------------------------------------------------- 1 | PostViewsCounterManual={init:function(a){let b={action:"pvc-view-posts",pvc_nonce:a.nonce,ids:a.ids},c=Object.keys(b).map(function(a){return encodeURIComponent(a)+"="+encodeURIComponent(b[a])}).join("&").replace(/%20/g,"+"),d=this;return fetch(a.url,{method:"POST",mode:"cors",cache:"no-cache",credentials:"same-origin",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=utf-8"},body:c}).then(function(a){if(!a.ok)throw Error(a.statusText);return a.json()}).then(function(a){try{"object"==typeof a&&null!==a&&d.triggerEvent("pvcCheckPost",a)}catch(b){console.log("Invalid JSON data"),console.log(b)}}).catch(function(a){console.log("Invalid response"),console.log(a)})},triggerEvent:function(a,b){let c=new CustomEvent(a,{bubbles:!0,detail:b});document.dispatchEvent(c)}} -------------------------------------------------------------------------------- /css/frontend.min.css: -------------------------------------------------------------------------------- 1 | .post-views.entry-meta>span{margin-right:0!important;font:16px;line-height:1}.post-views.entry-meta>span.post-views-icon.dashicons{display:inline-block;font-size:16px;line-height:1;text-decoration:inherit;vertical-align:middle}.post-views.load-dynamic .post-views-count{color:#fff0;transition:color 0.3s ease-in-out;position:relative}.post-views.load-dynamic.loaded .post-views-count{color:inherit}.post-views.load-dynamic.loading .post-views-count,.post-views.load-dynamic.loading .post-views-count:after{box-sizing:border-box}.post-views.load-dynamic .post-views-count:after{opacity:0;transition:opacity 0.3s ease-in-out;position:relative;color:#6610f2}.post-views.load-dynamic.loading .post-views-count:after{content:'';display:block;width:16px;height:16pxpx;border-radius:50%;border:2px solid currentColor;border-color:currentColor #fff0 currentColor #fff0;animation:pvc-loading 1s linear infinite;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);opacity:1}@keyframes pvc-loading{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}} -------------------------------------------------------------------------------- /css/frontend.css: -------------------------------------------------------------------------------- 1 | /* Frontend CSS */ .post-views.entry-meta > span { margin-right: 0 !important; font: 16px; line-height: 1; } .post-views.entry-meta > span.post-views-icon.dashicons { display: inline-block; font-size: 16px; line-height: 1; text-decoration: inherit; vertical-align: middle; } .post-views.load-dynamic .post-views-count { color: rgba(0, 0, 0, 0); transition: color 0.3s ease-in-out; position: relative; } .post-views.load-dynamic.loaded .post-views-count { color: inherit; } .post-views.load-dynamic.loading .post-views-count, .post-views.load-dynamic.loading .post-views-count:after { box-sizing: border-box; } .post-views.load-dynamic .post-views-count:after { opacity: 0; transition: opacity 0.3s ease-in-out; position: relative; color: rgb(102, 16, 242); } .post-views.load-dynamic.loading .post-views-count:after { content: ''; display: block; width: 16px; height: 16pxpx; border-radius: 50%; border: 2px solid currentColor; border-color: currentColor transparent currentColor transparent; animation: pvc-loading 1s linear infinite; position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); opacity: 1; } @keyframes pvc-loading { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "post-views-counter", 3 | "version": "1.3.11", 4 | "description": "Post Views Counter Gutenberg javascript.", 5 | "main": "gutenberg.js", 6 | "scripts": { 7 | "build": "webpack --env NODE_ENV=development --config webpack.config.js", 8 | "build:watch": "webpack --env NODE_ENV=development --config webpack.config.js --watch", 9 | "build:stage": "webpack --env NODE_ENV=stage --config webpack.config.js", 10 | "build:prod": "webpack --env NODE_ENV=production --config webpack.config.js" 11 | }, 12 | "author": "Digital Factory", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@babel/core": "^7.24.7", 16 | "@babel/preset-env": "^7.24.7", 17 | "@babel/preset-react": "^7.18.6", 18 | "babel-loader": "^9.1.3", 19 | "babel-plugin-transform-react-jsx": "^6.24.1", 20 | "css-loader": "^7.1.2", 21 | "extract-loader": "^5.1.0", 22 | "fast-deep-equal": "^3.1.3", 23 | "file-loader": "^6.2.0", 24 | "mini-css-extract-plugin": "^2.9.2", 25 | "node-sass": "^9.0.0", 26 | "postcss-loader": "^8.1.1", 27 | "sass": "^1.87.0", 28 | "sass-loader": "^16.0.5", 29 | "style-loader": "^4.0.0", 30 | "webpack": "^5.92.0", 31 | "webpack-cli": "^5.1.4", 32 | "webpack-fix-style-only-entries": "^0.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /js/admin-post.js: -------------------------------------------------------------------------------- 1 | ( function( $ ) { 2 | 3 | // ready event 4 | $( function() { 5 | // post views input 6 | $( '#post-views .edit-post-views' ).on( 'click', function() { 7 | if ( $( '#post-views-input-container' ).is( ":hidden" ) ) { 8 | $( '#post-views-input-container' ).slideDown( 'fast' ); 9 | $( this ).hide(); 10 | } 11 | 12 | return false; 13 | } ); 14 | 15 | // save post views 16 | $( '#post-views .save-post-views' ).on( 'click', function() { 17 | var views = ( $( '#post-views-display b' ).text() ).trim(); 18 | 19 | $( '#post-views-input-container' ).slideUp( 'fast' ); 20 | $( '#post-views .edit-post-views' ).show(); 21 | 22 | views = parseInt( $( '#post-views-input' ).val() ); 23 | // reassign value as integer 24 | $( '#post-views-input' ).val( views ); 25 | 26 | $( '#post-views-display b' ).text( views ); 27 | 28 | return false; 29 | } ); 30 | 31 | // cancel post views 32 | $( '#post-views .cancel-post-views' ).on( 'click', function() { 33 | var views = ( $( '#post-views-display b' ).text() ).trim(); 34 | 35 | $( '#post-views-input-container' ).slideUp( 'fast' ); 36 | $( '#post-views .edit-post-views' ).show(); 37 | 38 | views = parseInt( $( '#post-views-current' ).val() ); 39 | 40 | $( '#post-views-display b' ).text( views ); 41 | $( '#post-views-input' ).val( views ); 42 | 43 | return false; 44 | } ); 45 | } ); 46 | 47 | } )( jQuery ); -------------------------------------------------------------------------------- /blocks/post-views/build/index.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";var e={n:o=>{var t=o&&o.__esModule?()=>o.default:()=>o;return e.d(t,{a:t}),t},d:(o,t)=>{for(var r in t)e.o(t,r)&&!e.o(o,r)&&Object.defineProperty(o,r,{enumerable:!0,get:t[r]})},o:(e,o)=>Object.prototype.hasOwnProperty.call(e,o)};const o=window.wp.blocks,t=window.wp.components,r=window.wp.i18n,s=window.wp.blockEditor,n=window.wp.serverSideRender;var i=e.n(n);const p=window.ReactJSXRuntime;(0,o.registerBlockType)("post-views-counter/post-views",{edit:function({attributes:e,setAttributes:o}){const{postID:n,period:c}=e;return(0,p.jsxs)(p.Fragment,{children:[(0,p.jsx)(s.InspectorControls,{children:(0,p.jsxs)(t.PanelBody,{title:(0,r.__)("Settings","post-views-counter"),children:[(0,p.jsx)(t.TextControl,{__nextHasNoMarginBottom:!0,label:(0,r.__)("Post ID","post-views-counter"),value:n,onChange:e=>o({postID:Number(e)}),help:(0,r.__)("Enter 0 to use current visited post.","post-views-counter")}),(0,p.jsx)(t.SelectControl,{__nextHasNoMarginBottom:!0,disabled:1===pvcBlockEditorData.periods.length,label:(0,r.__)("Views period","post-views-counter"),value:c,options:pvcBlockEditorData.periods,onChange:e=>o({period:e})})]})}),(0,p.jsx)("div",{...(0,s.useBlockProps)(),children:(0,p.jsx)(i(),{httpMethod:"POST",block:"post-views-counter/post-views",attributes:e,LoadingResponsePlaceholder:()=>(0,p.jsx)(t.Spinner,{}),ErrorResponsePlaceholder:e=>(0,p.jsx)(t.Notice,{status:"error",children:(0,r.__)("Something went wrong. Try again or refresh the page.","post-views-counter")})})})]})}})})(); -------------------------------------------------------------------------------- /blocks/most-viewed-posts/src/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.wp.org/trunk/block.json", 3 | "apiVersion": 3, 4 | "name": "post-views-counter/most-viewed-posts", 5 | "version": "1.0.0", 6 | "title": "Most Viewed Posts", 7 | "category": "post-views-counter", 8 | "icon": "list-view", 9 | "description": "Displays a list of the most viewed posts.", 10 | "example": {}, 11 | "attributes": { 12 | "title": { 13 | "type": "string", 14 | "default": "Most Viewed Posts" 15 | }, 16 | "postTypes": { 17 | "type": "object", 18 | "default": { 19 | "post": true 20 | } 21 | }, 22 | "period": { 23 | "type": "string", 24 | "default": "total" 25 | }, 26 | "numberOfPosts": { 27 | "type": "integer", 28 | "default": 5 29 | }, 30 | "noPostsMessage": { 31 | "type": "string", 32 | "default": "No most viewed posts found." 33 | }, 34 | "order": { 35 | "type": "string", 36 | "default": "desc" 37 | }, 38 | "listType": { 39 | "type": "string", 40 | "default": "unordered" 41 | }, 42 | "displayPostViews": { 43 | "type": "boolean", 44 | "default": false 45 | }, 46 | "displayPostExcerpt": { 47 | "type": "boolean", 48 | "default": false 49 | }, 50 | "displayPostAuthor": { 51 | "type": "boolean", 52 | "default": false 53 | }, 54 | "displayPostThumbnail": { 55 | "type": "boolean", 56 | "default": false 57 | }, 58 | "thumbnailSize": { 59 | "type": "string", 60 | "default": "thumbnail" 61 | } 62 | }, 63 | "supports": { 64 | "html": false 65 | }, 66 | "textdomain": "post-views-counter", 67 | "editorScript": "file:./index.js" 68 | } 69 | -------------------------------------------------------------------------------- /blocks/most-viewed-posts/build/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schemas.wp.org/trunk/block.json", 3 | "apiVersion": 3, 4 | "name": "post-views-counter/most-viewed-posts", 5 | "version": "1.0.0", 6 | "title": "Most Viewed Posts", 7 | "category": "post-views-counter", 8 | "icon": "list-view", 9 | "description": "Displays a list of the most viewed posts.", 10 | "example": {}, 11 | "attributes": { 12 | "title": { 13 | "type": "string", 14 | "default": "Most Viewed Posts" 15 | }, 16 | "postTypes": { 17 | "type": "object", 18 | "default": { 19 | "post": true 20 | } 21 | }, 22 | "period": { 23 | "type": "string", 24 | "default": "total" 25 | }, 26 | "numberOfPosts": { 27 | "type": "integer", 28 | "default": 5 29 | }, 30 | "noPostsMessage": { 31 | "type": "string", 32 | "default": "No most viewed posts found." 33 | }, 34 | "order": { 35 | "type": "string", 36 | "default": "desc" 37 | }, 38 | "listType": { 39 | "type": "string", 40 | "default": "unordered" 41 | }, 42 | "displayPostViews": { 43 | "type": "boolean", 44 | "default": false 45 | }, 46 | "displayPostExcerpt": { 47 | "type": "boolean", 48 | "default": false 49 | }, 50 | "displayPostAuthor": { 51 | "type": "boolean", 52 | "default": false 53 | }, 54 | "displayPostThumbnail": { 55 | "type": "boolean", 56 | "default": false 57 | }, 58 | "thumbnailSize": { 59 | "type": "string", 60 | "default": "thumbnail" 61 | } 62 | }, 63 | "supports": { 64 | "html": false 65 | }, 66 | "textdomain": "post-views-counter", 67 | "editorScript": "file:./index.js" 68 | } -------------------------------------------------------------------------------- /js/counter.js: -------------------------------------------------------------------------------- 1 | PostViewsCounterManual = { 2 | /** 3 | * Initialize counter. 4 | * 5 | * @param {object} args 6 | * 7 | * @return {void} 8 | */ 9 | init: function( args ) { 10 | let params = { 11 | action: 'pvc-view-posts', 12 | pvc_nonce: args.nonce, 13 | ids: args.ids 14 | }; 15 | 16 | let newParams = Object.keys( params ).map( function( el ) { 17 | // add extra "data" array 18 | return encodeURIComponent( el ) + '=' + encodeURIComponent( params[el] ); 19 | } ).join( '&' ).replace( /%20/g, '+' ); 20 | 21 | const _this = this; 22 | 23 | return fetch( args.url, { 24 | method: 'POST', 25 | mode: 'cors', 26 | cache: 'no-cache', 27 | credentials: 'same-origin', 28 | headers: { 29 | 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' 30 | }, 31 | body: newParams 32 | } ).then( function( response ) { 33 | // invalid response? 34 | if ( ! response.ok ) 35 | throw Error( response.statusText ); 36 | 37 | return response.json(); 38 | } ).then( function( response ) { 39 | try { 40 | if ( typeof response === 'object' && response !== null ) 41 | _this.triggerEvent( 'pvcCheckPost', response ); 42 | } catch( error ) { 43 | console.log( 'Invalid JSON data' ); 44 | console.log( error ); 45 | } 46 | } ).catch( function( error ) { 47 | console.log( 'Invalid response' ); 48 | console.log( error ); 49 | } ); 50 | }, 51 | 52 | /** 53 | * Prepare the data to be sent with the request. 54 | * 55 | * @param {string} eventName 56 | * @param {object} data 57 | * 58 | * @return {void} 59 | */ 60 | triggerEvent: function( eventName, data ) { 61 | const newEvent = new CustomEvent( eventName, { 62 | bubbles: true, 63 | detail: data 64 | } ); 65 | 66 | // trigger event 67 | document.dispatchEvent( newEvent ); 68 | } 69 | } -------------------------------------------------------------------------------- /blocks/post-views/src/edit.js: -------------------------------------------------------------------------------- 1 | import { Notice, Spinner, PanelBody, TextControl, SelectControl } from '@wordpress/components'; 2 | import { __ } from '@wordpress/i18n'; 3 | import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; 4 | import ServerSideRender from '@wordpress/server-side-render'; 5 | 6 | export default function Edit( { attributes, setAttributes } ) { 7 | // attributes 8 | const { postID, period } = attributes; 9 | 10 | // spinner 11 | const spinner = () => { 12 | return ; 13 | } 14 | 15 | const error = ( value ) => { 16 | return { __( 'Something went wrong. Try again or refresh the page.', 'post-views-counter' ) }; 17 | } 18 | 19 | return ( 20 | <> 21 | 22 | 23 | setAttributes( { postID: Number( value ) } ) } 28 | help={ __( 'Enter 0 to use current visited post.', 'post-views-counter' ) } 29 | /> 30 | setAttributes( { period: value } ) } 37 | /> 38 | 39 | 40 |
41 | 48 |
49 | 50 | ) 51 | } -------------------------------------------------------------------------------- /js/frontend.min.js: -------------------------------------------------------------------------------- 1 | var initPostViewsCounter=function(){(PostViewsCounter={promise:null,args:{},init:function(e){this.args=e;var t={};t.storage_type="cookies",t.storage_data=this.readCookieData("pvc_visits"+(!1!==e.multisite?"_"+parseInt(e.multisite):"")),"rest_api"===e.mode?this.promise=this.request(e.requestURL,t,"POST",{"Content-Type":"application/x-www-form-urlencoded; charset=utf-8","X-WP-Nonce":e.nonce}):(t.action="pvc-check-post",t.pvc_nonce=e.nonce,t.id=e.postID,this.promise=this.request(e.requestURL,t,"POST",{"Content-Type":"application/x-www-form-urlencoded; charset=utf-8"}))},request:function(e,t,o,i){var n={method:o,mode:"cors",cache:"no-cache",credentials:"same-origin",headers:i,body:this.prepareRequestData(t)},a=this;return fetch(e,n).then(function(e){if(!e.ok)throw Error(e.statusText);return e.json()}).then(function(e){try{"object"==typeof e&&null!==e?"success"in e&&!1===e.success?(console.log("PVC: Request error."),console.log(e.data)):(a.saveCookieData(e.storage),a.triggerEvent("pvcCheckPost",e)):(console.log("PVC: Invalid object."),console.log(e))}catch(t){console.log("PVC: Invalid JSON data."),console.log(t)}}).catch(function(e){console.log("PVC: Invalid response."),console.log(e)})},prepareRequestData:function(e){return Object.keys(e).map(function(t){return encodeURIComponent(t)+"="+encodeURIComponent(e[t])}).join("&").replace(/%20/g,"+")},triggerEvent:function(e,t){var o=new CustomEvent(e,{bubbles:!0,detail:t});document.dispatchEvent(o)},saveCookieData:function(e){if(e.hasOwnProperty("name")){var t="";"https:"===document.location.protocol&&(t=";secure");for(var o=0;o 0 ) { 20 | // define the edit row 21 | var editRow = $( '#edit-' + postId ); 22 | var postRow = $( '#post-' + postId ); 23 | 24 | // get the data 25 | var postViews = $( '.column-post_views', postRow ).text(); 26 | 27 | // populate the data 28 | $( ':input[name="post_views"]', editRow ).val( postViews ); 29 | $( ':input[name="current_post_views"]', editRow ).val( postViews ); 30 | } 31 | 32 | return false; 33 | }; 34 | 35 | $( document ).on( 'click', '#bulk_edit', function() { 36 | // define the bulk edit row 37 | var bulkRow = $( '#bulk-edit' ); 38 | 39 | // get the selected post ids that are being edited 40 | var postIds = []; 41 | 42 | // at least wp 5.9? 43 | if ( pvcArgsQuickEdit.wpVersion59 ) { 44 | bulkRow.find( '#bulk-titles-list' ).children( '.ntdelitem' ).each( function() { 45 | postIds.push( $( this ).find( 'button' ).attr( 'id' ).replace( /[^0-9]/i, '' ) ); 46 | } ); 47 | } else { 48 | bulkRow.find( '#bulk-titles' ).children().each( function() { 49 | postIds.push( $( this ).attr( 'id' ).replace( /^(ttle)/i, '' ) ); 50 | } ); 51 | } 52 | 53 | // get the data 54 | var postViews = bulkRow.find( 'input[name="post_views"]' ).val(); 55 | 56 | // save the data 57 | $.ajax( { 58 | url: ajaxurl, // this is a variable that WordPress has already defined for us 59 | type: 'post', 60 | async: false, 61 | cache: false, 62 | data: { 63 | action: 'save_bulk_post_views', // this is the name of our WP AJAX function that we'll set up next 64 | post_ids: postIds, // and these are the 2 parameters we're passing to our function 65 | post_views: postViews, 66 | current_post_views: postViews, 67 | nonce: pvcArgsQuickEdit.nonce 68 | } 69 | } ); 70 | } ); 71 | } ); 72 | 73 | } )( jQuery ); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Post Views Counter 2 | 3 | Post Views Counter allows you to display how many times a post, page or custom post type had been viewed in a simple, fast and reliable way. 4 | 5 | ## Description 6 | 7 | [Post Views Counter](http://www.dfactory.co/products/post-views-counter/) allows you to display how many times a post, page or custom post type had been viewed in a simple, fast and reliable way. 8 | 9 | For more information, check out plugin page at [dFactory](http://www.dfactory.co/) or plugin [support forum](http://www.dfactory.co/support/forum/post-views-counter/). 10 | 11 | ### Features include: 12 | 13 | * Option to select post types for which post views will be counted and displayed. 14 | * 3 methods of collecting post views data: PHP, Javascript or REST API for greater flexibility 15 | * Possibility to manually set views count for each post 16 | * Dashboard post views stats widget 17 | * Capability to query posts according to its views count 18 | * Custom REST API endpoints 19 | * Option to set counts interval 20 | * Excluding counts from visitors: bots, logged in users, selected user roles 21 | * Excluding users by IPs 22 | * Restricting display by user roles 23 | * Restricting post views editing to admins 24 | * One-click data import from WP-PostViews 25 | * Sortable admin column 26 | * Post views display position, automatic or manual via shortcode 27 | * W3 Cache/WP SuperCache compatible 28 | * Optional object cache support 29 | * WPML and Polylang compatible 30 | * .pot file for translations included 31 | 32 | ## Installation 33 | 34 | 1. Install Post Views Counter either via the WordPress.org plugin directory, or by uploading the files to your server 35 | 2. Activate the plugin through the 'Plugins' menu in WordPress 36 | 3. Go to the Post Views Counter settings and set your options. 37 | 38 | ## Changelog 39 | 40 | #### 1.3 41 | * New: Gutenberg compatibility 42 | * New: Additional options in widgets: post author and display style 43 | * Fix: Undefined variables when IP saving enabled 44 | * Fix: Check cookie not being triggered in Fast Ajax mode 45 | * Fix: Invalid arguments in implode function causing warning 46 | * Fix: Thumbnail size option did not show up after thumbnail checkbox was checked 47 | * Fix: Saving post (in quick edit mode too) did not update post views 48 | 49 | #### 1.2.14 50 | * Fix: Bulk edit post views count reset issue 51 | 52 | #### 1.2.13 53 | * New: Experimental Fast AJAX counter method (10+ times faster) 54 | 55 | #### 1.2.12 56 | * New: GDPR compatibility with Cookie Notice plugin 57 | 58 | #### 1.2.11 59 | * Tweak: Additional IP expiration checks added as an option 60 | 61 | #### 1.2.10 62 | * New: Additional transient based IP expiration checks 63 | * Tweak: Chart.js script update to 2.7.1 64 | 65 | #### 1.2.9 66 | * Fix: WooCommerce products list table broken 67 | 68 | #### 1.2.8 69 | * New: Multisite compatibility 70 | * Fix: Undefined index post_views_column on post_views_counter/includes/settings.php 71 | * Tweak: Improved user IP handling 72 | 73 | #### 1.2.7 74 | * Fix: Chart data not updating for object cached installs due to missing expire parameter 75 | * Fix: Bug preventing hiding the counter based on user role. 76 | * Fix: Undefined notice in the admin dashboard request -------------------------------------------------------------------------------- /includes/class-cron.php: -------------------------------------------------------------------------------- 1 | 1, 39 | 'weeks' => 7, 40 | 'months' => 30, 41 | 'years' => 365 42 | ]; 43 | 44 | // get main instance 45 | $pvc = Post_Views_Counter(); 46 | 47 | // default where clause 48 | $where = [ 'type = 0', 'CAST( period AS SIGNED ) < CAST( ' . date( 'Ymd', strtotime( '-' . ( (int) ( $counter[$pvc->options['general']['reset_counts']['type']] * $pvc->options['general']['reset_counts']['number'] ) ) . ' days' ) ) . ' AS SIGNED)' ]; 49 | 50 | // update where clause 51 | $where = apply_filters( 'pvc_reset_counts_where_clause', $where ); 52 | 53 | // delete views 54 | $wpdb->query( 'DELETE FROM ' . $wpdb->prefix . 'post_views WHERE ' . implode( ' AND ', $where ) ); 55 | } 56 | 57 | /** 58 | * Add new cron interval from settings. 59 | * 60 | * @param array $schedules 61 | * @return array 62 | */ 63 | public function cron_time_intervals( $schedules ) { 64 | // get main instance 65 | $pvc = Post_Views_Counter(); 66 | 67 | $schedules['post_views_counter_interval'] = [ 68 | 'interval' => DAY_IN_SECONDS, 69 | 'display' => __( 'Post Views Counter reset daily counts interval', 'post-views-counter' ) 70 | ]; 71 | 72 | return $schedules; 73 | } 74 | 75 | /** 76 | * Check whether WP Cron needs to add new task. 77 | * 78 | * @return void 79 | */ 80 | public function check_cron() { 81 | // only for backend 82 | if ( ! is_admin() ) 83 | return; 84 | 85 | // get main instance 86 | $pvc = Post_Views_Counter(); 87 | 88 | // set wp cron task 89 | if ( $pvc->options['general']['cron_run'] ) { 90 | // not set or need to be updated? 91 | if ( ! wp_next_scheduled( 'pvc_reset_counts' ) || $pvc->options['general']['cron_update'] ) { 92 | // task is added but need to be updated 93 | if ( $pvc->options['general']['cron_update'] ) { 94 | // remove old schedule 95 | wp_clear_scheduled_hook( 'pvc_reset_counts' ); 96 | 97 | // set update to false 98 | $general = $pvc->options['general']; 99 | $general['cron_update'] = false; 100 | 101 | // update settings 102 | update_option( 'post_views_counter_settings_general', $general ); 103 | } 104 | 105 | // set schedule 106 | wp_schedule_event( current_time( 'timestamp', true ) + DAY_IN_SECONDS, 'post_views_counter_interval', 'pvc_reset_counts' ); 107 | } 108 | } else { 109 | // remove schedule 110 | wp_clear_scheduled_hook( 'pvc_reset_counts' ); 111 | 112 | remove_action( 'pvc_reset_counts', [ $this, 'reset_counts' ] ); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /blocks/most-viewed-posts/build/index.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";var e={n:o=>{var t=o&&o.__esModule?()=>o.default:()=>o;return e.d(t,{a:t}),t},d:(o,t)=>{for(var s in t)e.o(t,s)&&!e.o(o,s)&&Object.defineProperty(o,s,{enumerable:!0,get:t[s]})},o:(e,o)=>Object.prototype.hasOwnProperty.call(e,o)};const o=window.wp.blocks,t=window.React,s=window.wp.components,n=window.wp.i18n,r=window.wp.blockEditor,a=window.wp.serverSideRender;var i=e.n(a);const l=window.ReactJSXRuntime;(0,o.registerBlockType)("post-views-counter/most-viewed-posts",{edit:function({attributes:e,setAttributes:o}){const{title:a,postTypes:c,period:p,numberOfPosts:d,noPostsMessage:_,order:u,listType:w,displayPostViews:h,displayPostExcerpt:v,displayPostAuthor:b,displayPostThumbnail:x,thumbnailSize:g}=e,[m,C]=(0,t.useState)(c||pvcBlockEditorData.postTypesKeys);return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(r.InspectorControls,{children:(0,l.jsxs)(s.PanelBody,{title:(0,n.__)("Settings","post-views-counter"),children:[(0,l.jsx)(s.TextControl,{__nextHasNoMarginBottom:!0,label:(0,n.__)("Title","post-views-counter"),value:a,onChange:e=>o({title:e}),help:(0,n.__)("Enter empty text to hide title.","post-views-counter")}),(0,l.jsx)(s.BaseControl,{__nextHasNoMarginBottom:!0,label:(0,n.__)("Post types","post-views-counter"),children:Object.keys(pvcBlockEditorData.postTypes).map((e=>(0,l.jsx)(s.CheckboxControl,{label:pvcBlockEditorData.postTypes[e],checked:m[e],onChange:t=>{let s={...c};s[e]=t,C((o=>({...o,[e]:!o[e]}))),o({postTypes:s})}},e)))}),(0,l.jsx)(s.SelectControl,{__nextHasNoMarginBottom:!0,disabled:1===pvcBlockEditorData.periods.length,label:(0,n.__)("Views period","post-views-counter"),value:p,options:pvcBlockEditorData.periods,onChange:e=>o({period:e})}),(0,l.jsx)(s.TextControl,{__nextHasNoMarginBottom:!0,label:(0,n.__)("Number of posts to show","post-views-counter"),value:d,onChange:e=>o({numberOfPosts:Number(e)})}),(0,l.jsx)(s.TextControl,{__nextHasNoMarginBottom:!0,label:(0,n.__)("No posts message","post-views-counter"),value:_,onChange:e=>o({noPostsMessage:e})}),(0,l.jsx)(s.RadioControl,{label:(0,n.__)("Order","post-views-counter"),selected:u,options:[{label:(0,n.__)("Ascending","post-views-counter"),value:"asc"},{label:(0,n.__)("Descending","post-views-counter"),value:"desc"}],onChange:e=>o({order:e})}),(0,l.jsx)(s.RadioControl,{label:(0,n.__)("Display style","post-views-counter"),selected:w,options:[{label:(0,n.__)("Unordered list","post-views-counter"),value:"unordered"},{label:(0,n.__)("Ordered list","post-views-counter"),value:"ordered"}],onChange:e=>o({listType:e})}),(0,l.jsxs)(s.BaseControl,{__nextHasNoMarginBottom:!0,label:(0,n.__)("Display Data","post-views-counter"),children:[(0,l.jsx)(s.CheckboxControl,{__nextHasNoMarginBottom:!0,label:(0,n.__)("Post views","post-views-counter"),checked:h,onChange:e=>o({displayPostViews:e})}),(0,l.jsx)(s.CheckboxControl,{__nextHasNoMarginBottom:!0,label:(0,n.__)("Post excerpt","post-views-counter"),checked:v,onChange:e=>o({displayPostExcerpt:e})}),(0,l.jsx)(s.CheckboxControl,{__nextHasNoMarginBottom:!0,label:(0,n.__)("Post author","post-views-counter"),checked:b,onChange:e=>o({displayPostAuthor:e})}),(0,l.jsx)(s.CheckboxControl,{__nextHasNoMarginBottom:!0,label:(0,n.__)("Post thumbnail","post-views-counter"),checked:x,onChange:e=>o({displayPostThumbnail:e})}),x&&(0,l.jsx)(s.SelectControl,{__nextHasNoMarginBottom:!0,label:(0,n.__)("Thumbnail size","post-views-counter"),value:g,options:pvcBlockEditorData.imageSizes,onChange:e=>o({thumbnailSize:e})})]})]})}),(0,l.jsx)("div",{...(0,r.useBlockProps)(),children:(0,l.jsx)(i(),{httpMethod:"POST",block:"post-views-counter/most-viewed-posts",attributes:e,LoadingResponsePlaceholder:()=>(0,l.jsx)(s.Spinner,{}),ErrorResponsePlaceholder:e=>(0,l.jsx)(s.Notice,{status:"error",children:(0,n.__)("Something went wrong. Try again or refresh the page.","post-views-counter")})})})]})}})})(); -------------------------------------------------------------------------------- /css/admin-dashboard.min.css: -------------------------------------------------------------------------------- 1 | .pvc-dashboard-container{min-height:260px;margin:0 -4px;position:relative;display:flex;flex-direction:column}.pvc-dashboard-container *{box-sizing:border-box}.pvc-dashboard-container .spinner{position:absolute;left:50%;top:40%;margin-left:-10px;z-index:10}.pvc-dashboard-container.loading{pointer-events:none}.pvc-dashboard-container.loading:after{position:absolute;content:'';display:block;height:100%;width:100%;top:0;left:0;background-color:rgb(255 255 255 / .8);z-index:1;transition:all 0.2s}.pvc-dashboard p.sub{color:#8f8f8f;font-size:14px;text-align:left;padding-bottom:3px;border-bottom:1px solid #ececec}.pvc-dashboard-container .pvc-date-nav{display:flex;justify-content:space-between;color:#aaa;width:100%}.pvc-dashboard-container .pvc-date-nav .current{color:#212529}.pvc-dashboard-content-top{padding:0 6px}.pvc-dashboard-content-bottom{padding:0 6px}.pvc-data-container{min-height:230px}.pvc-data-container a{text-decoration:none}.pvc-table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}.pvc-table{caption-side:bottom;border-collapse:collapse;width:100%;margin-bottom:1rem;color:inherit;vertical-align:top;border-color:#dee2e6;text-align:left}.pvc-table>thead{vertical-align:bottom;color:#212529}.pvc-table>tbody{vertical-align:inherit}.pvc-table tbody,.pvc-table td,.pvc-table tfoot,.pvc-table th,.pvc-table thead,.pvc-table tr{border-color:inherit;border-style:solid;border-width:0}.pvc-table th,.pvc-table td{text-align:inherit;text-align:-webkit-match-parent}.pvc-table th:first-child,.pvc-table td:first-child{width:1px;white-space:nowrap}.pvc-table th:last-child,.pvc-table td:last-child{text-align:right}.pvc-table .no-posts :last-child{text-align:left}.pvc-table>:not(caption)>*>*{padding:.5rem .5rem;background-color:#fff0;border-bottom-width:1px;box-shadow:inset 0 0 0 9999px #fff0}.pvc-table .cn-edit-link{display:none}.pvc-table .cn-edit-link:before{content:'(';display:inline-block}.pvc-table .cn-edit-link:after{content:')';display:inline-block}.pvc-table tr:hover .cn-edit-link{display:inline-block}#pvc_dashboard .inside{margin:0;padding:0}.pvc-accordion-header{display:flex;align-items:center;justify-content:space-between;background-color:#fafafa;border-bottom:1px solid #eee}.pvc-accordion-toggle{cursor:pointer;line-height:1;position:relative;font-size:14px;font-weight:400;margin:0;padding:11px 12px;color:#23282c;flex-grow:1}.pvc-accordion-title{display:inline-block;padding-right:10px}.pvc-accordion-actions{z-index:1;height:14px;line-height:1;flex-shrink:0;padding:11px 6px}.pvc-accordion-actions .pvc-accordion-action{width:14px;text-align:center}.pvc-accordion-actions .pvc-accordion-action,.pvc-accordion-actions .pvc-accordion-action::before{font-size:14px;height:14px;line-height:1;color:#72777c;display:inline-block;text-decoration:none!important}.pvc-accordion-actions .pvc-accordion-action::before{width:14px}.pvc-accordion-actions .pvc-toggle-indicator::before{color:#72777c;content:"\f142";display:inline-block;font-family:dashicons;line-height:1;transform:rotate(0deg);speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-indent:-1px}.pvc-tooltip{position:relative}.pvc-tooltip-icon{display:inline-block;width:16px;cursor:help}.pvc-tooltip-icon::before{color:#b4b9be;content:"\f14c";display:inline-block;font:normal 16px dashicons;line-height:1;position:absolute;text-decoration:none!important;speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;left:0;top:2px}.pvc-according-header{display:flex;align-items:center;justify-content:space-between}.pvc-accordion-content{padding:11px 12px;border-bottom:1px solid #eee;height:100%}.pvc-collapsed .pvc-toggle-indicator::before{transform:rotate(180deg)}.pvc-collapsed .pvc-accordion-content{display:none}.pvc-dashboard-block{display:flex;justify-content:center;align-items:center;background:#fafafa;color:#787c82;font-size:13px;font-style:italic;padding:13px;margin-top:0} -------------------------------------------------------------------------------- /src/admin.scss: -------------------------------------------------------------------------------- 1 | /* Post Views Counter settings */ 2 | .post-views-counter-settings { 3 | margin-right: 300px; 4 | 5 | form { 6 | float: left; 7 | min-width: 463px; 8 | width: auto; 9 | } 10 | 11 | fieldset .description { 12 | font-size: 13px; 13 | margin-bottom: 8px; 14 | margin-top: 4px; 15 | display: block; 16 | } 17 | 18 | p.help, 19 | p.description, 20 | span.description { 21 | font-size: 13px; 22 | font-style: italic; 23 | } 24 | 25 | .ip-box { 26 | margin-bottom: 3px; 27 | } 28 | 29 | .pvc_user_roles { 30 | margin-top: 12px; 31 | } 32 | 33 | select { 34 | vertical-align: top; 35 | } 36 | 37 | output { 38 | display: block; 39 | font-size: 30px; 40 | font-weight: bold; 41 | text-align: center; 42 | margin: 30px 0; 43 | width: 100%; 44 | } 45 | 46 | .df-credits { 47 | float: right; 48 | width: 280px; 49 | background: #fff; 50 | margin: 20px -300px 20px 20px; 51 | position: relative; 52 | box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05); 53 | 54 | .inner { 55 | padding-left: 10px; 56 | padding-right: 10px; 57 | } 58 | 59 | h3 { 60 | font-size: 14px; 61 | line-height: 1.4; 62 | margin: 0; 63 | padding: 8px 12px; 64 | border-bottom: 1px solid #eee; 65 | } 66 | 67 | .df-link { 68 | padding-top: 5px; 69 | padding-bottom: 10px; 70 | margin: 0; 71 | 72 | a { 73 | display: block; 74 | text-align: center; 75 | outline: none !important; 76 | border: none !important; 77 | box-shadow: none !important; 78 | 79 | img { 80 | display: block; 81 | margin: 0 auto; 82 | width: 80px; 83 | } 84 | } 85 | } 86 | 87 | hr { 88 | border: solid #eee; 89 | border-width: 1px 0 0; 90 | clear: both; 91 | height: 0; 92 | } 93 | } 94 | } 95 | 96 | /* Single post edit screen */ 97 | #misc-publishing-actions #post-views { 98 | #post-views-display:before { 99 | display: inline-block; 100 | font: 400 20px/1 dashicons; 101 | left: -1px; 102 | padding: 0 2px 0 0; 103 | position: relative; 104 | text-decoration: none !important; 105 | vertical-align: top; 106 | color: #888; 107 | content: "\f185"; 108 | top: -1px; 109 | } 110 | } 111 | 112 | /* Listing edit screen */ 113 | .edit-php { 114 | .widefat { 115 | th { 116 | &#post_views { 117 | width: 5.5em; 118 | } 119 | 120 | &.column-post_views .dashicons { 121 | &, 122 | &:before { 123 | font-size: 1.1em; 124 | vertical-align: middle; 125 | } 126 | } 127 | 128 | &.dash-title { 129 | display: none; 130 | } 131 | } 132 | 133 | td { 134 | .dashicons { 135 | &, 136 | &:before { 137 | font-size: 1.1em; 138 | } 139 | } 140 | } 141 | } 142 | 143 | .metabox-prefs { 144 | .dash-icon { 145 | display: none; 146 | } 147 | } 148 | 149 | #inline-edit-post_views { 150 | input { 151 | width: auto; 152 | } 153 | } 154 | 155 | .is-hidden { 156 | display: none !important; 157 | visibility: hidden !important; 158 | } 159 | } 160 | 161 | .upload-php { 162 | .widefat { 163 | .dash-title { 164 | display: none; 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /js/admin-settings.js: -------------------------------------------------------------------------------- 1 | ( function( $ ) { 2 | 3 | // ready event 4 | $( function() { 5 | var ip_boxes = $( '#post_views_counter_general_exclude_ips_setting' ).find( '.ip-box' ).length; 6 | 7 | $( '#post_views_counter_general_exclude_ips_setting .ip-box:first' ).find( '.remove-exclude-ip' ).hide(); 8 | 9 | // ask whether to reset options to defaults 10 | $( document ).on( 'click', '.reset_pvc_settings', function() { 11 | var result = confirm( pvcArgsSettings.resetToDefaults ); 12 | 13 | if ( result && $( this ).hasClass( 'reset_post_views_counter_settings_other' ) ) 14 | $( 'input[data-pvc-menu="submenu"]' ).after( $( 'input[data-pvc-menu="topmenu"]' ) ); 15 | 16 | return result; 17 | } ); 18 | 19 | // ask whether to reset views 20 | $( document ).on( 'click', 'input[name="post_views_counter_reset_views"]', function() { 21 | return confirm( pvcArgsSettings.resetViews ); 22 | } ); 23 | 24 | // ask whether to import views 25 | $( document ).on( 'click', 'input[name="post_views_counter_import_views"]', function() { 26 | return confirm( pvcArgsSettings.importViews ); 27 | } ); 28 | 29 | // remove ip box 30 | $( document ).on( 'click', '.remove-exclude-ip', function( e ) { 31 | e.preventDefault(); 32 | 33 | ip_boxes--; 34 | 35 | var parent = $( this ).parent(); 36 | 37 | // remove ip box 38 | parent.slideUp( 'fast', function() { 39 | $( this ).remove(); 40 | } ); 41 | } ); 42 | 43 | // add ip box 44 | $( document ).on( 'click', '.add-exclude-ip', function() { 45 | ip_boxes++; 46 | 47 | var parent = $( this ).parents( '#post_views_counter_general_exclude_ips_setting' ), 48 | new_ip_box = parent.find( '.ip-box:last' ).clone().hide(); 49 | 50 | // clear value 51 | new_ip_box.find( 'input' ).val( '' ); 52 | 53 | if ( ip_boxes > 1 ) 54 | new_ip_box.find( '.remove-exclude-ip' ).show(); 55 | 56 | // add and display new ip box 57 | parent.find( '.ip-box:last' ).after( new_ip_box ).next().slideDown( 'fast' ); 58 | } ); 59 | 60 | // add current ip 61 | $( document ).on( 'click', '.add-current-ip', function() { 62 | // fill input with user's current ip 63 | $( this ).parents( '#post_views_counter_general_exclude_ips_setting' ).find( '.ip-box' ).last().find( 'input' ).val( $( this ).attr( 'data-rel' ) ); 64 | } ); 65 | 66 | // toggle user roles 67 | $( '#pvc_exclude-roles, #pvc_restrict_display-roles' ).on( 'change', function() { 68 | if ( $( this ).is( ':checked' ) ) 69 | $( '.pvc_user_roles' ).slideDown( 'fast' ); 70 | else 71 | $( '.pvc_user_roles' ).slideUp( 'fast' ); 72 | } ); 73 | 74 | // menu position referer update 75 | $( 'input[name="post_views_counter_settings_display[menu_position]"], input[name="post_views_counter_settings_other[menu_position]"]' ).on( 'change', function() { 76 | if ( $( this ).val() === 'top' ) 77 | $( 'input[data-pvc-menu="submenu"]' ).after( $( 'input[data-pvc-menu="topmenu"]' ) ); 78 | else 79 | $( 'input[data-pvc-menu="submenu"]' ).before( $( 'input[data-pvc-menu="topmenu"]' ) ); 80 | } ); 81 | 82 | // import provider switching 83 | $( 'input[name="pvc_import_provider"]' ).on( 'change', function() { 84 | var selectedProvider = $( this ).val(), 85 | $current = $( '.pvc-provider-content:visible' ), 86 | $target = $( '.pvc-provider-' + selectedProvider ); 87 | 88 | if ( ! $target.length || $target.is( ':visible' ) ) 89 | return; 90 | 91 | $current.stop( true, true ).slideUp( 'fast', function() { 92 | $target.stop( true, true ).slideDown( 'fast' ); 93 | } ); 94 | } ); 95 | 96 | // import strategy description switching 97 | ( function() { 98 | var $container = $( '.pvc-import-strategy-details' ), 99 | $strategyRadios = $( 'input[name="pvc_import_strategy"]' ); 100 | 101 | if ( ! $container.length || ! $strategyRadios.length ) 102 | return; 103 | 104 | var showStrategy = function( slug ) { 105 | var $target = $container.find( '.pvc-strategy-' + slug ); 106 | 107 | if ( ! $target.length ) 108 | return; 109 | 110 | var $current = $container.find( '.pvc-strategy-content:visible' ); 111 | 112 | if ( $target.is( ':visible' ) ) 113 | return; 114 | 115 | $current.stop( true, true ).slideUp( 'fast', function() { 116 | $target.stop( true, true ).slideDown( 'fast' ); 117 | } ); 118 | }; 119 | 120 | $strategyRadios.on( 'change', function() { 121 | showStrategy( $( this ).val() ); 122 | } ); 123 | 124 | showStrategy( $strategyRadios.filter( ':checked' ).val() ); 125 | } )(); 126 | } ); 127 | 128 | } )( jQuery ); 129 | -------------------------------------------------------------------------------- /includes/class-functions.php: -------------------------------------------------------------------------------- 1 | true ], 'objects', 'and' ) as $key => $post_type ) { 30 | $post_types[$key] = $post_type->labels->name; 31 | } 32 | 33 | // remove bbPress replies 34 | if ( class_exists( 'bbPress' ) && isset( $post_types['reply'] ) ) 35 | unset( $post_types['reply'] ); 36 | 37 | // filter post types 38 | $post_types = apply_filters( 'pvc_available_post_types', $post_types ); 39 | 40 | // sort post types alphabetically 41 | asort( $post_types, SORT_STRING ); 42 | 43 | return $post_types; 44 | } 45 | 46 | /** 47 | * Get all user roles. 48 | * 49 | * @global object $wp_roles 50 | * 51 | * @return array 52 | */ 53 | public function get_user_roles() { 54 | global $wp_roles; 55 | 56 | $roles = []; 57 | 58 | foreach ( apply_filters( 'editable_roles', $wp_roles->roles ) as $role => $details ) { 59 | $roles[$role] = translate_user_role( $details['name'] ); 60 | } 61 | 62 | // sort user roles alphabetically 63 | asort( $roles, SORT_STRING ); 64 | 65 | return $roles; 66 | } 67 | 68 | /** 69 | * Get taxonomies available for counting. 70 | * 71 | * @param bool $mode 72 | * @return array 73 | */ 74 | public function get_taxonomies( $mode = 'labels' ) { 75 | // get public taxonomies 76 | $taxonomies = get_taxonomies( 77 | [ 78 | 'public' => true 79 | ], 80 | $mode === 'keys' ? 'names' : 'objects', 81 | 'and' 82 | ); 83 | 84 | // only keys 85 | if ( $mode === 'keys' ) 86 | $_taxonomies = array_keys( $taxonomies ); 87 | // objects 88 | elseif ( $mode === 'objects' ) 89 | $_taxonomies = $taxonomies; 90 | // labels 91 | else { 92 | $_taxonomies = []; 93 | 94 | // prepare taxonomy labels 95 | foreach ( $taxonomies as $name => $taxonomy ) { 96 | $_taxonomies[$name] = $taxonomy->label; 97 | } 98 | } 99 | 100 | return $_taxonomies; 101 | } 102 | 103 | /** 104 | * Get color scheme. 105 | * 106 | * @global array $_wp_admin_css_colors 107 | * 108 | * @return string 109 | */ 110 | public function get_current_scheme_color( $default_color = '' ) { 111 | // get color scheme global 112 | global $_wp_admin_css_colors; 113 | 114 | // set default color; 115 | $color = '#2271b1'; 116 | 117 | if ( ! empty( $_wp_admin_css_colors ) ) { 118 | // get current admin color scheme name 119 | $current_color_scheme = get_user_option( 'admin_color' ); 120 | 121 | if ( empty( $current_color_scheme ) ) 122 | $current_color_scheme = 'fresh'; 123 | 124 | $wp_scheme_colors = [ 125 | 'coffee' => 2, 126 | 'ectoplasm' => 2, 127 | 'ocean' => 2, 128 | 'sunrise' => 2, 129 | 'midnight' => 3, 130 | 'blue' => 3, 131 | 'modern' => 1, 132 | 'light' => 1, 133 | 'fresh' => 2 134 | ]; 135 | 136 | // one of default wp schemes? 137 | if ( array_key_exists( $current_color_scheme, $wp_scheme_colors ) ) { 138 | $color_number = $wp_scheme_colors[$current_color_scheme]; 139 | 140 | // color exists? 141 | if ( isset( $_wp_admin_css_colors[$current_color_scheme] ) && property_exists( $_wp_admin_css_colors[$current_color_scheme], 'colors' ) && isset( $_wp_admin_css_colors[$current_color_scheme]->colors[$color_number] ) ) 142 | $color = $_wp_admin_css_colors[$current_color_scheme]->colors[$color_number]; 143 | } 144 | } 145 | 146 | return sanitize_hex_color( apply_filters( 'pvc_current_scheme_color', $color ) ); 147 | } 148 | 149 | /** 150 | * Convert HEX to RGB color. 151 | * 152 | * @param string $color 153 | * @return bool|array 154 | */ 155 | public function hex2rgb( $color ) { 156 | if ( ! is_string( $color ) ) 157 | return false; 158 | 159 | // with hash? 160 | if ( $color[0] === '#' ) 161 | $color = substr( $color, 1 ); 162 | 163 | if ( sanitize_hex_color_no_hash( $color ) !== $color ) 164 | return false; 165 | 166 | // 6 hex digits? 167 | if ( strlen( $color ) === 6 ) 168 | list( $r, $g, $b ) = [ $color[0] . $color[1], $color[2] . $color[3], $color[4] . $color[5] ]; 169 | // 3 hex digits? 170 | elseif ( strlen( $color ) === 3 ) 171 | list( $r, $g, $b ) = [ $color[0] . $color[0], $color[1] . $color[1], $color[2] . $color[2] ]; 172 | else 173 | return false; 174 | 175 | return [ 'r' => hexdec( $r ), 'g' => hexdec( $g ), 'b' => hexdec( $b ) ]; 176 | } 177 | 178 | /** 179 | * Get default color. 180 | * 181 | * @return array 182 | */ 183 | public function get_colors() { 184 | // get current color scheme 185 | $color = $this->get_current_scheme_color(); 186 | 187 | // convert it to rgb 188 | $color = $this->hex2rgb( $color ); 189 | 190 | // invalid color? 191 | if ( $color === false ) { 192 | // set default color 193 | $color = [ 'r' => 34, 'g' => 113, 'b' => 177 ]; 194 | } 195 | 196 | return $color; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /js/frontend.js: -------------------------------------------------------------------------------- 1 | var initPostViewsCounter = function() { 2 | PostViewsCounter = { 3 | promise: null, 4 | args: {}, 5 | 6 | /** 7 | * Initialize counter. 8 | * 9 | * @param {Object} args 10 | * @return {void} 11 | */ 12 | init: function( args ) { 13 | this.args = args; 14 | 15 | // default parameters 16 | var params = {}; 17 | 18 | // data storage 19 | params.storage_type = 'cookies'; 20 | params.storage_data = this.readCookieData( 'pvc_visits' + ( args.multisite !== false ? '_' + parseInt( args.multisite ) : '' ) ); 21 | 22 | // rest api request 23 | if ( args.mode === 'rest_api' ) { 24 | this.promise = this.request( args.requestURL, params, 'POST', { 25 | 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 26 | 'X-WP-Nonce': args.nonce 27 | } ); 28 | // ajax request 29 | } else { 30 | params.action = 'pvc-check-post'; 31 | params.pvc_nonce = args.nonce; 32 | params.id = args.postID; 33 | 34 | this.promise = this.request( args.requestURL, params, 'POST', { 35 | 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' 36 | } ); 37 | } 38 | }, 39 | 40 | /** 41 | * Handle fetch request. 42 | * 43 | * @param {string} url 44 | * @param {Object} params 45 | * @param {string} method 46 | * @param {Object} headers 47 | * @return {Promise} 48 | */ 49 | request: function( url, params, method, headers ) { 50 | var options = { 51 | method: method, 52 | mode: 'cors', 53 | cache: 'no-cache', 54 | credentials: 'same-origin', 55 | headers: headers, 56 | body: this.prepareRequestData( params ) 57 | }; 58 | 59 | var _this = this; 60 | 61 | return fetch( url, options ).then( function( response ) { 62 | // invalid response? 63 | if ( ! response.ok ) 64 | throw Error( response.statusText ); 65 | 66 | return response.json(); 67 | } ).then( function( response ) { 68 | try { 69 | if ( typeof response === 'object' && response !== null ) { 70 | if ( 'success' in response && response.success === false ) { 71 | console.log( 'PVC: Request error.' ); 72 | console.log( response.data ); 73 | } else { 74 | _this.saveCookieData( response.storage ); 75 | 76 | _this.triggerEvent( 'pvcCheckPost', response ); 77 | } 78 | } else { 79 | console.log( 'PVC: Invalid object.' ); 80 | console.log( response ); 81 | } 82 | } catch( error ) { 83 | console.log( 'PVC: Invalid JSON data.' ); 84 | console.log( error ); 85 | } 86 | } ).catch( function( error ) { 87 | console.log( 'PVC: Invalid response.' ); 88 | console.log( error ); 89 | } ); 90 | }, 91 | 92 | /** 93 | * Prepare the data to be sent with the request. 94 | * 95 | * @param {Object} data 96 | * @return {string} 97 | */ 98 | prepareRequestData: function( data ) { 99 | return Object.keys( data ).map( function( el ) { 100 | // add extra "data" array 101 | return encodeURIComponent( el ) + '=' + encodeURIComponent( data[el] ); 102 | } ).join( '&' ).replace( /%20/g, '+' ); 103 | }, 104 | 105 | /** 106 | * Trigger a custom DOM event. 107 | * 108 | * @param {string} eventName 109 | * @param {Object} data 110 | * @return {void} 111 | */ 112 | triggerEvent: function( eventName, data ) { 113 | var newEvent = new CustomEvent( eventName, { 114 | bubbles: true, 115 | detail: data 116 | } ); 117 | 118 | // trigger event 119 | document.dispatchEvent( newEvent ); 120 | }, 121 | 122 | /** 123 | * Save cookies. 124 | * 125 | * @param {Object} data 126 | * @return {void} 127 | */ 128 | saveCookieData: function( data ) { 129 | // empty storage? nothing to save 130 | if ( ! data.hasOwnProperty( 'name' ) ) 131 | return; 132 | 133 | var cookieSecure = ''; 134 | 135 | // ssl? 136 | if ( document.location.protocol === 'https:' ) 137 | cookieSecure = ';secure'; 138 | 139 | for ( var i = 0; i < data.name.length; i++ ) { 140 | var cookieDate = new Date(); 141 | var expiration = parseInt( data.expiry[i] ); 142 | 143 | // valid expiration timestamp? 144 | if ( expiration ) 145 | expiration = expiration * 1000; 146 | // add default 24 hours 147 | else 148 | expiration = cookieDate.getTime() + 86400000; 149 | 150 | // set cookie date expiry 151 | cookieDate.setTime( expiration ); 152 | 153 | // set cookie 154 | document.cookie = data.name[i] + '=' + data.value[i] + ';expires=' + cookieDate.toUTCString() + ';path=/' + ( this.args.path === '/' ? '' : this.args.path ) + ';domain=' + this.args.domain + cookieSecure + ';SameSite=Lax'; 155 | } 156 | }, 157 | 158 | /** 159 | * Read cookies. 160 | * 161 | * @param {string} name 162 | * @return {string} 163 | */ 164 | readCookieData: function( name ) { 165 | var cookies = []; 166 | 167 | document.cookie.split( ';' ).forEach( function( el ) { 168 | var parts = el.split( '=' ); 169 | var key = parts[0]; 170 | var value = parts[1]; 171 | var trimmedKey = key.trim(); 172 | var regex = new RegExp( name + '\\[\\d+\\]' ); 173 | 174 | // look all cookies starts with name parameter 175 | if ( regex.test( trimmedKey ) ) 176 | cookies.push( value ); 177 | } ); 178 | 179 | return cookies.join( 'a' ); 180 | } 181 | } 182 | 183 | PostViewsCounter.init( pvcArgsFrontend ); 184 | } 185 | 186 | document.addEventListener( 'DOMContentLoaded', initPostViewsCounter ); -------------------------------------------------------------------------------- /js/block-editor.min.js: -------------------------------------------------------------------------------- 1 | (()=>{function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}function t(e,t){for(var i=0;idiv:not(:last-child){margin-bottom:3em;}.post-views-sidebar .inner img{max-width:80%;height:auto;display:block;margin:20px auto;}.post-views-counter-settings p.help,.post-views-counter-settings p.description{font-size:13px;font-style:italic;line-height:1.6;}.post-views-counter-settings div.ip-box{margin-bottom:3px;}.post-views-counter-settings select{vertical-align:top;}.post-views-counter-settings .available{color:#00a32a;}.post-views-counter-settings .unavailable{color:#d63638;}.pvc-status-table .pvc-status{display:inline-block;font-size:0.95em;}.pvc-status-table .pvc-status-active{color:#00a32a;}.pvc-status-table .pvc-status-missing{color:#d63638;}.pvc-subfield{margin-top:12px;}#misc-publishing-actions #post-views #post-views-display:before{display:inline-block;font:400 20px/1 dashicons;left:-1px;padding:0 2px 0 0;position:relative;text-decoration:none !important;vertical-align:top;color:#888;content:"\f185";top:-1px;}.edit-php .widefat th#post_views{width:5.5em;}.edit-php .widefat th.column-post_views .dashicons,.edit-php .widefat th.column-post_views .dashicons:before{font-size:1.1em;vertical-align:middle;}.edit-php .widefat th .dash-title,.upload-php .widefat th .dash-title{display:none;}.edit-php .metabox-prefs .dash-icon{display:none;}.edit-php .widefat td .dashicons,.edit-php .widefat td .dashicons:before{font-size:1.1em;}.edit-php #inline-edit-post_views input{width:auto;}.is-hidden{display:none !important;visibility:hidden !important}output{display:block;font-size:30px;font-weight:bold;text-align:center;margin:30px 0;width:100%;}.post-views-credits{background:#fff;box-shadow:0 0 0 1px rgba(0,0,0,0.05);}.post-views-credits .inner{text-align:center;margin:0;}.post-views-counter-settings .pvc-button{color:#fff;background-color:#6610f2;border-color:#6610f2;}.post-views-counter-settings .pvc-button:active,.post-views-counter-settings .pvc-button:focus,.post-views-counter-settings .pvc-button:hover{color:#fff;background-color:#570ece;border-color:#570ece;}.post-views-counter-settings .pvc-button:focus{box-shadow:0 0 0 1px #fff,0 0 0 3px #6610f2;}.post-views-credits h2{border:none;padding-bottom:0;font-size:23px;font-weight:normal;margin:0.25em 0 0.5em;color:#6610f2;}.post-views-credits h3{font-size:18px;line-height:1.4;font-weight:normal;margin:0;padding:0;color:#6610f2;}.post-views-credits p:first-child{margin-top:0;}.post-views-credits .pvc-sidebar-title{font-size:17px;font-weight:bold;margin:10px 0 20px;}.post-views-credits .pvc-sidebar-body{padding-bottom:0;font-size:14px;text-align:left;margin:2em 0;padding:0;}.post-views-credits .pvc-sidebar-footer{margin:1em 0;}.post-views-credits .pvc-sidebar-body p{padding-left:20px;margin:0.75em 0;position:relative;}.post-views-credits .pvc-sidebar-body b{font-weight:bold;color:#000;}.post-views-credits .pvc-sidebar-body .pvc-icon{position:absolute;top:0;left:0;}.post-views-credits .pvc-sidebar-body .pvc-icon-check{box-sizing:border-box;display:block;transform:scale(1);width:16px;height:22px;border-radius:100px;}.post-views-credits .pvc-sidebar-body .pvc-icon-check::after{content:"";display:block;box-sizing:border-box;position:absolute;left:0;top:0;width:6px;height:10px;border-width:0 2px 2px 0;border-style:solid;transform-origin:bottom left;transform:rotate(45deg);}#post_views_counter_other_license_setting .pvc-status-icon{vertical-align:middle;margin-left:8px;padding-bottom: 3px;}#post_views_counter_other_license_setting .pvc-status-icon:before{content:"✗";color:#d63638}#post_views_counter_other_license_setting.license-status .pvc-status-icon:before{content:"✗";color:#d63638}#post_views_counter_other_license_setting.license-status.valid .pvc-status-icon:before{content:"✓";color:#00a32a}#pvc-reports-upgrade{position:absolute;left:0;top:0;height:100%;width:100%;overflow:hidden;box-sizing:border-box;min-height:400px;}#pvc-reports-bg{width:100%;height:auto;opacity:0.8;filter:blur(2px);}#pvc-reports-modal{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);padding:1.5em 3em;box-shadow:0 0 25px 10px rgba(0,0,0,0.1);border-radius:3px;background-color:#fff;text-align:center;width:26em;}#pvc-reports-modal p{margin:0;}#pvc-reports-modal h2{font-size:21px;font-weight:400;margin:0 0 10px 0;padding:9px 0 4px;line-height:1.3;}#pvc-reports-modal .button{margin-top:25px;margin-bottom:10px;}@media only screen and (max-width:960px){.post-views-counter-settings{flex-wrap:wrap;}.post-views-counter-settings .post-views-sidebar{width:100%;}}.pvc-provider-radio{display:inline-block;margin-right:20px;font-weight:normal;}.pvc-provider-radio input[type="radio"]{margin-right:5px;}.pvc-provider-disabled{opacity:0.6;cursor:not-allowed;}.pvc-provider-disabled input[type="radio"]{cursor:not-allowed;}.pvc-provider-content{margin:0;}.pvc-provider-fields{padding:0;margin-top:15px;}.pvc-provider-fields label{font-weight:600;margin-bottom:5px;display:block;}.pvc-provider-fields input.regular-text{margin-top:5px;}.pvc-provider-unavailable{color:#d63638;font-style:italic;}.pvc-import-strategy{margin-bottom:25px;}.post-views-counter-settings tr.pvc-pro-extended label[for="pvc_import_strategy_skip_existing"]:after,.post-views-counter-settings tr.pvc-pro-extended label[for="pvc_import_strategy_keep_higher_count"]:after,.post-views-counter-settings tr.pvc-pro-extended label[for="pvc_import_strategy_fill_empty_only"]:after{content:"PRO";display:inline-block;margin-left:6px;padding:1px 4px;font-size:11px;border-radius:4px;background-color:#ffc107;color:#fff;font-weight:600;}.pvc-field-group label{margin-right:8px;}.pvc-radio-vertical label{display:block;margin:5px 0;font-weight:normal;}.pvc-radio-vertical input[type="radio"]{margin-right:5px;}.pvc-import-actions{padding-top:10px;}.pvc-import-actions .button{margin-right:10px;}.pvc-import-actions .button-primary{font-weight:600;} -------------------------------------------------------------------------------- /blocks/most-viewed-posts/src/edit.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Notice, Spinner, PanelBody, TextControl, CheckboxControl, BaseControl, RadioControl, SelectControl } from '@wordpress/components'; 3 | import { __ } from '@wordpress/i18n'; 4 | import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; 5 | import ServerSideRender from '@wordpress/server-side-render'; 6 | 7 | export default function Edit( { attributes, setAttributes } ) { 8 | // attributes 9 | const { title, postTypes, period, numberOfPosts, noPostsMessage, order, listType, displayPostViews, displayPostExcerpt, displayPostAuthor, displayPostThumbnail, thumbnailSize } = attributes; 10 | 11 | // initialize post types state 12 | const [checkedState, setCheckedState] = useState( ! postTypes ? pvcBlockEditorData.postTypesKeys : postTypes ); 13 | 14 | // spinner 15 | const spinner = () => { 16 | return ; 17 | } 18 | 19 | const error = ( value ) => { 20 | return { __( 'Something went wrong. Try again or refresh the page.', 'post-views-counter' ) }; 21 | } 22 | 23 | return ( 24 | <> 25 | 26 | 27 | setAttributes( { title: value } ) } 32 | help={ __( 'Enter empty text to hide title.', 'post-views-counter' ) } 33 | /> 34 | 38 | { ( 39 | Object.keys( pvcBlockEditorData.postTypes ).map( ( postType ) => ( 40 | { 45 | // clone postTypes, we cant change attribute value directly 46 | let newValue = {...postTypes} 47 | 48 | // set new value 49 | newValue[postType] = value 50 | 51 | // set state and attribute 52 | setCheckedState( ( prevState ) => ( { ...prevState, [postType]: ! prevState[postType] } ) ) 53 | setAttributes( { postTypes: newValue } ) 54 | } } 55 | /> 56 | ) ) 57 | ) } 58 | 59 | setAttributes( { period: value } ) } 66 | /> 67 | setAttributes( { numberOfPosts: Number( value ) } ) } 72 | /> 73 | setAttributes( { noPostsMessage: value } ) } 78 | /> 79 | setAttributes( { order: value } ) } 87 | /> 88 | setAttributes( { listType: value } ) } 96 | /> 97 | 101 | setAttributes( { displayPostViews: value } ) } 106 | /> 107 | setAttributes( { displayPostExcerpt: value } ) } 112 | /> 113 | setAttributes( { displayPostAuthor: value } ) } 118 | /> 119 | setAttributes( { displayPostThumbnail: value } ) } 124 | /> 125 | { displayPostThumbnail && setAttributes( { thumbnailSize: value } ) } 131 | /> } 132 | 133 | 134 | 135 |
136 | 143 |
144 | 145 | ) 146 | } -------------------------------------------------------------------------------- /src/admin-dashboard.scss: -------------------------------------------------------------------------------- 1 | #pvc_dashboard { 2 | .inside { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | } 7 | 8 | .pvc-accordion { 9 | .pvc-accordion-item { 10 | &.pvc-collapsed .pvc-accordion-toggle::before { 11 | transform: rotate(180deg); 12 | } 13 | 14 | &.pvc-collapsed .pvc-accordion-content { 15 | display: none; 16 | } 17 | .pvc-according-header { 18 | display: flex; 19 | align-items: center; 20 | justify-content: space-between; 21 | } 22 | 23 | .pvc-accordion-content { 24 | padding: 11px 12px; 25 | border-bottom: 1px solid #eee; 26 | height: 100%; 27 | } 28 | } 29 | 30 | .pvc-accordion-toggle { 31 | background-color: #fafafa; 32 | border-bottom: 1px solid #eee; 33 | cursor: pointer; 34 | line-height: 1; 35 | position: relative; 36 | font-size: 14px; 37 | font-weight: 400; 38 | line-height: 1; 39 | margin: 0; 40 | padding: 11px 12px; 41 | color: #23282c; 42 | 43 | &:before { 44 | color: #72777c; 45 | content: "\f142"; 46 | display: inline-block; 47 | font: normal 20px/1 dashicons; 48 | position: absolute; 49 | right: 8px; 50 | text-decoration: none !important; 51 | text-indent: -1px; 52 | transform: rotate(0deg); 53 | speak: none; 54 | -webkit-font-smoothing: antialiased; 55 | -moz-osx-font-smoothing: grayscale; 56 | top: 8px; 57 | } 58 | } 59 | 60 | .pvc-accordion-title { 61 | display: inline-block; 62 | padding-right: 10px; 63 | } 64 | 65 | .pvc-accordion-actions { 66 | position: absolute; 67 | top: 0; 68 | right: 0; 69 | z-index: 1; 70 | padding: 11px 30px 11px 0; 71 | height: 14px; 72 | line-height: 1; 73 | 74 | .pvc-accordion-action { 75 | &, 76 | &:before { 77 | font-size: 14px; 78 | height: 14px; 79 | width: 14px; 80 | color: #72777c; 81 | } 82 | } 83 | } 84 | 85 | .pvc-tooltip { 86 | position: relative; 87 | } 88 | 89 | .pvc-tooltip-icon { 90 | display: inline-block; 91 | width: 16px; 92 | cursor: help; 93 | 94 | &:before { 95 | color: #b4b9be; 96 | content: "\f14c"; 97 | display: inline-block; 98 | font: normal 16px/1 dashicons; 99 | position: absolute; 100 | text-decoration: none !important; 101 | speak: none; 102 | -webkit-font-smoothing: antialiased; 103 | -moz-osx-font-smoothing: grayscale; 104 | left: 0; 105 | top: 2px; 106 | } 107 | } 108 | } 109 | 110 | .pvc-dashboard-container { 111 | min-height: 260px; 112 | margin: 0 -4px; 113 | text-align: center; 114 | position: relative; 115 | display: flex; 116 | flex-direction: column; 117 | 118 | &.loading { 119 | pointer-events: none; 120 | 121 | &:after { 122 | position: absolute; 123 | content: ""; 124 | display: block; 125 | height: 100%; 126 | width: 100%; 127 | top: 0; 128 | left: 0; 129 | background-color: rgba(255, 255, 255, 0.8); 130 | z-index: 1; 131 | transition: all 0.2s; 132 | } 133 | } 134 | 135 | .spinner { 136 | position: absolute; 137 | left: 50%; 138 | top: 40%; 139 | margin-left: -10px; 140 | z-index: 10; 141 | } 142 | 143 | .pvc-months { 144 | display: flex; 145 | justify-content: space-between; 146 | margin: 6px 10px 0 10px; 147 | color: #aaa; 148 | 149 | .current { 150 | color: #212529; 151 | } 152 | } 153 | 154 | .pvc-dashboard { 155 | p.sub { 156 | color: #8f8f8f; 157 | font-size: 14px; 158 | text-align: left; 159 | padding-bottom: 3px; 160 | border-bottom: 1px solid #ececec; 161 | } 162 | } 163 | 164 | .pvc-data-container { 165 | min-height: 230px; 166 | } 167 | 168 | .pvc-table-responsive { 169 | overflow-x: auto; 170 | -webkit-overflow-scrolling: touch; 171 | } 172 | 173 | .pvc-table { 174 | caption-side: bottom; 175 | border-collapse: collapse; 176 | width: 100%; 177 | margin-bottom: 1rem; 178 | color: inherit; 179 | vertical-align: top; 180 | border-color: #dee2e6; 181 | text-align: left; 182 | 183 | > thead { 184 | vertical-align: bottom; 185 | color: #212529; 186 | } 187 | 188 | > tbody { 189 | vertical-align: inherit; 190 | } 191 | 192 | tbody, 193 | td, 194 | tfoot, 195 | th, 196 | thead, 197 | tr { 198 | border-color: inherit; 199 | border-style: solid; 200 | border-width: 0; 201 | } 202 | 203 | th, 204 | td { 205 | text-align: inherit; 206 | text-align: -webkit-match-parent; 207 | 208 | &:first-child { 209 | width: 1px; 210 | white-space: nowrap; 211 | } 212 | 213 | &:last-child { 214 | text-align: right; 215 | } 216 | } 217 | 218 | .no-posts :last-child { 219 | text-align: left; 220 | } 221 | 222 | > :not(caption) > * > * { 223 | padding: 0.5rem 0.5rem; 224 | background-color: transparent; 225 | border-bottom-width: 1px; 226 | box-shadow: inset 0 0 0 9999px transparent; 227 | } 228 | } 229 | } -------------------------------------------------------------------------------- /css/admin-dashboard.css: -------------------------------------------------------------------------------- 1 | .pvc-dashboard-container { 2 | min-height: 260px; 3 | margin: 0 -4px; 4 | position: relative; 5 | display: flex; 6 | flex-direction: column; 7 | } 8 | 9 | .pvc-dashboard-container * { 10 | box-sizing: border-box; 11 | } 12 | .pvc-dashboard-container .spinner { 13 | position: absolute; 14 | left: 50%; 15 | top: 40%; 16 | margin-left: -10px; 17 | z-index: 10; 18 | } 19 | .pvc-dashboard-container.loading { 20 | pointer-events: none; 21 | } 22 | .pvc-dashboard-container.loading:after { 23 | position: absolute; 24 | content: ''; 25 | display: block; 26 | height: 100%; 27 | width: 100%; 28 | top: 0; 29 | left: 0; 30 | background-color: rgba(255,255,255,0.8); 31 | z-index: 1; 32 | transition: all 0.2s; 33 | } 34 | .pvc-dashboard p.sub { 35 | color: #8f8f8f; 36 | font-size: 14px; 37 | text-align: left; 38 | padding-bottom: 3px; 39 | border-bottom: 1px solid #ececec; 40 | } 41 | .pvc-dashboard-container .pvc-date-nav { 42 | display: flex; 43 | justify-content: space-between; 44 | color: #aaa; 45 | width: 100%; 46 | } 47 | .pvc-dashboard-container .pvc-date-nav .current { 48 | color: #212529; 49 | } 50 | .pvc-dashboard-content-top { 51 | padding: 0 6px; 52 | } 53 | .pvc-dashboard-content-bottom { 54 | padding: 0 6px; 55 | } 56 | .pvc-data-container { 57 | min-height: 230px; 58 | } 59 | .pvc-data-container a { 60 | text-decoration: none; 61 | } 62 | .pvc-table-responsive { 63 | overflow-x: auto; 64 | -webkit-overflow-scrolling: touch; 65 | } 66 | .pvc-table { 67 | caption-side: bottom; 68 | border-collapse: collapse; 69 | width: 100%; 70 | margin-bottom: 1rem; 71 | color: inherit; 72 | vertical-align: top; 73 | border-color: #dee2e6; 74 | text-align: left; 75 | } 76 | .pvc-table > thead { 77 | vertical-align: bottom; 78 | color: #212529; 79 | } 80 | .pvc-table > tbody { 81 | vertical-align: inherit; 82 | } 83 | .pvc-table tbody, 84 | .pvc-table td, 85 | .pvc-table tfoot, 86 | .pvc-table th, 87 | .pvc-table thead, 88 | .pvc-table tr { 89 | border-color: inherit; 90 | border-style: solid; 91 | border-width: 0; 92 | } 93 | .pvc-table th, 94 | .pvc-table td { 95 | text-align: inherit; 96 | text-align: -webkit-match-parent; 97 | } 98 | .pvc-table th:first-child, 99 | .pvc-table td:first-child { 100 | width: 1px; 101 | white-space: nowrap; 102 | } 103 | .pvc-table th:last-child, 104 | .pvc-table td:last-child { 105 | text-align: right; 106 | } 107 | .pvc-table .no-posts :last-child { 108 | text-align: left; 109 | } 110 | .pvc-table > :not(caption) > * > * { 111 | padding: .5rem .5rem; 112 | background-color: transparent; 113 | border-bottom-width: 1px; 114 | box-shadow: inset 0 0 0 9999px transparent; 115 | } 116 | .pvc-table .cn-edit-link { 117 | display: none; 118 | } 119 | .pvc-table .cn-edit-link:before { 120 | content: '('; 121 | display: inline-block; 122 | } 123 | .pvc-table .cn-edit-link:after { 124 | content: ')'; 125 | display: inline-block; 126 | } 127 | .pvc-table tr:hover .cn-edit-link { 128 | display: inline-block; 129 | } 130 | #pvc_dashboard .inside { 131 | margin: 0; 132 | padding: 0; 133 | } 134 | .pvc-accordion-header { 135 | display: flex; 136 | align-items: center; 137 | justify-content: space-between; 138 | background-color: #fafafa; 139 | border-bottom: 1px solid #eee; 140 | } 141 | .pvc-accordion-toggle { 142 | cursor: pointer; 143 | line-height: 1; 144 | position: relative; 145 | font-size: 14px; 146 | font-weight: 400; 147 | margin: 0; 148 | padding: 11px 12px; 149 | color: #23282c; 150 | flex-grow: 1; 151 | } 152 | .pvc-accordion-title { 153 | display: inline-block; 154 | padding-right: 10px; 155 | } 156 | .pvc-accordion-actions { 157 | z-index: 1; 158 | height: 14px; 159 | line-height: 1; 160 | flex-shrink: 0; 161 | padding: 11px 6px; 162 | } 163 | .pvc-accordion-actions .pvc-accordion-action { 164 | width: 14px; 165 | text-align: center; 166 | } 167 | .pvc-accordion-actions .pvc-accordion-action, 168 | .pvc-accordion-actions .pvc-accordion-action::before { 169 | font-size: 14px; 170 | height: 14px; 171 | line-height: 1; 172 | color: #72777c; 173 | display: inline-block; 174 | text-decoration: none !important; 175 | } 176 | .pvc-accordion-actions .pvc-accordion-action::before { 177 | width: 14px; 178 | } 179 | .pvc-accordion-actions .pvc-toggle-indicator::before { 180 | color: #72777c; 181 | content: "\f142"; 182 | display: inline-block; 183 | font-family: dashicons; 184 | line-height: 1; 185 | transform: rotate(0deg); 186 | speak: none; 187 | -webkit-font-smoothing: antialiased; 188 | -moz-osx-font-smoothing: grayscale; 189 | text-indent: -1px; 190 | } 191 | .pvc-tooltip { 192 | position: relative; 193 | } 194 | .pvc-tooltip-icon { 195 | display: inline-block; 196 | width: 16px; 197 | cursor: help; 198 | } 199 | .pvc-tooltip-icon::before { 200 | color: #b4b9be; 201 | content: "\f14c"; 202 | display: inline-block; 203 | font: normal 16px dashicons; 204 | line-height: 1; 205 | position: absolute; 206 | text-decoration: none !important; 207 | speak: none; 208 | -webkit-font-smoothing: antialiased; 209 | -moz-osx-font-smoothing: grayscale; 210 | left: 0; 211 | top: 2px; 212 | } 213 | .pvc-according-header { 214 | display: flex; 215 | align-items: center; 216 | justify-content: space-between; 217 | } 218 | .pvc-accordion-content { 219 | padding: 11px 12px; 220 | border-bottom: 1px solid #eee; 221 | height: 100%; 222 | } 223 | .pvc-collapsed .pvc-toggle-indicator::before { 224 | transform: rotate(180deg); 225 | } 226 | .pvc-collapsed .pvc-accordion-content { 227 | display: none; 228 | } 229 | .pvc-dashboard-block { 230 | display: flex; 231 | justify-content: center; 232 | align-items: center; 233 | background: #fafafa; 234 | color: #787c82; 235 | font-size: 13px; 236 | font-style: italic; 237 | padding: 13px; 238 | margin-top: 0; 239 | } -------------------------------------------------------------------------------- /css/column-modal.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Post Views Counter - Column Modal Chart Styles 3 | */ 4 | 5 | /* Micromodal base styles */ 6 | .pvc-modal { 7 | display: none; 8 | } 9 | 10 | .pvc-modal.is-open { 11 | display: block; 12 | } 13 | 14 | /* Modal overlay */ 15 | .pvc-modal__overlay { 16 | position: fixed; 17 | top: 0; 18 | left: 0; 19 | right: 0; 20 | bottom: 0; 21 | background: rgba(0, 0, 0, 0.7); 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | z-index: 100000; 26 | } 27 | 28 | /* Modal container */ 29 | .pvc-modal__container { 30 | background-color: #fff; 31 | padding: 0; 32 | max-width: 800px; 33 | width: 90%; 34 | max-height: 90vh; 35 | border-radius: 4px; 36 | overflow-y: auto; 37 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); 38 | position: relative; 39 | } 40 | 41 | /* Modal header */ 42 | .pvc-modal__header { 43 | display: flex; 44 | justify-content: space-between; 45 | align-items: center; 46 | padding: 15px 20px; 47 | border-bottom: 1px solid #dcdcde; 48 | } 49 | 50 | .pvc-modal__title { 51 | margin: 0; 52 | } 53 | 54 | .pvc-modal__close { 55 | background: transparent; 56 | border: none; 57 | font-size: 28px; 58 | font-weight: 400; 59 | line-height: 1; 60 | color: #50575e; 61 | cursor: pointer; 62 | padding: 0; 63 | display: flex; 64 | align-items: center; 65 | justify-content: center; 66 | transition: background-color 0.2s ease; 67 | } 68 | 69 | .pvc-modal__close:hover, 70 | .pvc-modal__close:focus { 71 | color: #1d2327; 72 | outline: none; 73 | } 74 | 75 | .pvc-modal__close::before { 76 | content: "\00d7"; 77 | margin-top: -4px; 78 | } 79 | 80 | /* Modal content */ 81 | .pvc-modal__content { 82 | padding: 15px 20px; 83 | } 84 | 85 | /* Modal stats */ 86 | .pvc-modal-content-top { 87 | margin-bottom: 20px; 88 | } 89 | 90 | .pvc-modal-count { 91 | font-weight: 100; 92 | font-size: 32px; 93 | line-height: 1; 94 | } 95 | 96 | /* Modal stats structure */ 97 | .pvc-modal-summary { 98 | display: flex; 99 | flex-direction: column; 100 | gap: 5px; 101 | } 102 | 103 | .pvc-modal-views-data { 104 | display: flex; 105 | align-items: center; 106 | gap: 10px; 107 | } 108 | 109 | /* Chart container */ 110 | .pvc-modal-chart-container { 111 | position: relative; 112 | height: 200px; 113 | margin-bottom: 20px; 114 | background-color: #fff; 115 | border-radius: 3px; 116 | } 117 | 118 | .pvc-modal-chart-container.loading { 119 | opacity: 0.5; 120 | pointer-events: none; 121 | } 122 | 123 | .pvc-modal-chart-container .spinner { 124 | position: absolute; 125 | top: 50%; 126 | left: 50%; 127 | transform: translate(-50%, -50%); 128 | display: none; 129 | margin: 0; 130 | float: none; 131 | } 132 | 133 | .pvc-modal-chart-container.loading .spinner { 134 | display: block; 135 | visibility: visible; 136 | } 137 | 138 | .pvc-modal-chart-container canvas { 139 | max-width: 100%; 140 | } 141 | 142 | /* Date navigation */ 143 | .pvc-modal-content-bottom { 144 | padding-top: 10px; 145 | border-top: 1px solid #dcdcde; 146 | } 147 | 148 | .pvc-modal-nav { 149 | display: flex; 150 | justify-content: space-between; 151 | align-items: center; 152 | width: 100%; 153 | } 154 | 155 | .pvc-modal-nav-prev, 156 | .pvc-modal-nav-next { 157 | text-decoration: none; 158 | font-size: 14px; 159 | padding: 6px 12px; 160 | border-radius: 3px; 161 | transition: background-color 0.2s ease; 162 | } 163 | 164 | .pvc-modal-nav-prev:hover, 165 | .pvc-modal-nav-next:hover { 166 | background-color: #f0f0f1; 167 | } 168 | 169 | .pvc-modal-nav-next.pvc-disabled { 170 | opacity: 0.5; 171 | cursor: default; 172 | pointer-events: none; 173 | } 174 | 175 | .pvc-modal-nav-current { 176 | font-weight: 600; 177 | font-size: 14px; 178 | } 179 | 180 | /* Error state */ 181 | .pvc-modal-error { 182 | padding: 15px 20px; 183 | text-align: center; 184 | color: #d63638; 185 | font-weight: 500; 186 | margin: 0 0 15px 0; 187 | background-color: #fcf0f1; 188 | border: 1px solid #f1a0a4; 189 | border-radius: 3px; 190 | } 191 | 192 | /* Animation for Micromodal */ 193 | @keyframes mmfadeIn { 194 | from { 195 | opacity: 0; 196 | } 197 | to { 198 | opacity: 1; 199 | } 200 | } 201 | 202 | @keyframes mmfadeOut { 203 | from { 204 | opacity: 1; 205 | } 206 | to { 207 | opacity: 0; 208 | } 209 | } 210 | 211 | @keyframes mmslideIn { 212 | from { 213 | transform: translateY(15%); 214 | } 215 | to { 216 | transform: translateY(0); 217 | } 218 | } 219 | 220 | @keyframes mmslideOut { 221 | from { 222 | transform: translateY(0); 223 | } 224 | to { 225 | transform: translateY(-10%); 226 | } 227 | } 228 | 229 | .micromodal-slide { 230 | display: none; 231 | } 232 | 233 | .micromodal-slide.is-open { 234 | display: block; 235 | } 236 | 237 | .micromodal-slide[aria-hidden="false"] .pvc-modal__overlay { 238 | animation: mmfadeIn 0.3s cubic-bezier(0, 0, 0.2, 1); 239 | } 240 | 241 | .micromodal-slide[aria-hidden="false"] .pvc-modal__container { 242 | animation: mmslideIn 0.3s cubic-bezier(0, 0, 0.2, 1); 243 | } 244 | 245 | .micromodal-slide[aria-hidden="true"] .pvc-modal__overlay { 246 | animation: mmfadeOut 0.3s cubic-bezier(0, 0, 0.2, 1); 247 | } 248 | 249 | .micromodal-slide[aria-hidden="true"] .pvc-modal__container { 250 | animation: mmslideOut 0.3s cubic-bezier(0, 0, 0.2, 1); 251 | } 252 | 253 | .micromodal-slide .pvc-modal__container, 254 | .micromodal-slide .pvc-modal__overlay { 255 | will-change: transform; 256 | } 257 | 258 | /* Responsive */ 259 | @media screen and (max-width: 768px) { 260 | .pvc-modal__container { 261 | width: 95%; 262 | max-height: 95vh; 263 | } 264 | 265 | .pvc-modal__header { 266 | padding: 15px; 267 | } 268 | 269 | .pvc-modal__title { 270 | font-size: 18px; 271 | padding-right: 10px; 272 | } 273 | 274 | .pvc-modal__content { 275 | padding: 15px; 276 | } 277 | 278 | .pvc-modal-chart-container { 279 | height: 180px; 280 | } 281 | 282 | .pvc-modal-nav { 283 | flex-direction: column; 284 | gap: 10px; 285 | } 286 | 287 | .pvc-modal-nav-prev, 288 | .pvc-modal-nav-next { 289 | width: 100%; 290 | text-align: center; 291 | } 292 | } 293 | 294 | /* Hide modal when printing */ 295 | @media print { 296 | .pvc-modal { 297 | display: none !important; 298 | } 299 | } -------------------------------------------------------------------------------- /includes/class-toolbar.php: -------------------------------------------------------------------------------- 1 | options['display']['toolbar_statistics'] ) ) 34 | return; 35 | 36 | // skip for not logged in users 37 | if ( ! is_user_logged_in() ) 38 | return; 39 | 40 | // skip users with turned off admin bar at frontend 41 | if ( ! is_admin() && get_user_option( 'show_admin_bar_front' ) !== 'true' ) 42 | return; 43 | 44 | if ( is_admin() ) 45 | add_action( 'admin_init', [ $this, 'admin_bar_maybe_add_style' ] ); 46 | else 47 | add_action( 'wp', [ $this, 'admin_bar_maybe_add_style' ] ); 48 | } 49 | 50 | /** 51 | * Add admin bar stats to a post. 52 | * 53 | * @global string $pagenow 54 | * @global string $post 55 | * 56 | * @param object $admin_bar 57 | * @return void 58 | */ 59 | public function admin_bar_menu( $admin_bar ) { 60 | // get main instance 61 | $pvc = Post_Views_Counter(); 62 | 63 | // set empty post 64 | $post = null; 65 | 66 | // admin? 67 | if ( is_admin() && ! wp_doing_ajax() ) { 68 | global $pagenow; 69 | 70 | $post = ( $pagenow === 'post.php' && ! empty( $_GET['post'] ) ) ? get_post( (int) $_GET['post'] ) : $post; 71 | // frontend? 72 | } elseif ( is_singular() ) 73 | global $post; 74 | 75 | // get countable post types 76 | $post_types = $pvc->options['general']['post_types_count']; 77 | 78 | // break if display is not allowed 79 | if ( empty( $post_types ) || empty( $post ) || ! in_array( $post->post_type, $post_types, true ) ) 80 | return; 81 | 82 | if ( apply_filters( 'pvc_admin_display_post_views', true ) === false ) 83 | return; 84 | 85 | $dt = new DateTime(); 86 | 87 | // get post views 88 | $views = pvc_get_views( 89 | [ 90 | 'post_id' => $post->ID, 91 | 'post_type' => $post->post_type, 92 | 'fields' => 'date=>views', 93 | 'views_query' => [ 94 | 'year' => $dt->format( 'Y' ), 95 | 'month' => $dt->format( 'm' ) 96 | ] 97 | ] 98 | ); 99 | 100 | $graph = ''; 101 | 102 | // get highest value 103 | $views_copy = $views; 104 | 105 | arsort( $views_copy, SORT_NUMERIC ); 106 | 107 | $highest = reset( $views_copy ); 108 | 109 | // find the multiplier 110 | $multiplier = $highest * 0.05; 111 | 112 | // generate ranges 113 | $ranges = []; 114 | 115 | for ( $i = 1; $i <= 20; $i ++ ) { 116 | $ranges[$i] = round( $multiplier * $i ); 117 | } 118 | 119 | // create graph 120 | foreach ( $views as $date => $count ) { 121 | $count_class = 0; 122 | 123 | if ( $count > 0 ) { 124 | foreach ( $ranges as $index => $range ) { 125 | if ( $count <= $range ) { 126 | $count_class = $index; 127 | break; 128 | } 129 | } 130 | } 131 | 132 | $graph .= ''; 133 | } 134 | 135 | $admin_bar->add_menu( 136 | [ 137 | 'id' => 'pvc-post-views', 138 | 'title' => '' . $graph . '', 139 | 'href' => false, 140 | 'meta' => [ 141 | 'title' => false 142 | ] 143 | ] 144 | ); 145 | } 146 | 147 | /** 148 | * Maybe add admin CSS. 149 | * 150 | * @global string $pagenow 151 | * @global string $post 152 | * 153 | * @return void 154 | */ 155 | public function admin_bar_maybe_add_style() { 156 | // get main instance 157 | $pvc = Post_Views_Counter(); 158 | 159 | // set empty post 160 | $post = null; 161 | 162 | // admin? 163 | if ( is_admin() && ! wp_doing_ajax() ) { 164 | global $pagenow; 165 | 166 | $post = ( $pagenow === 'post.php' && ! empty( $_GET['post'] ) ) ? get_post( (int) $_GET['post'] ) : $post; 167 | // frontend? 168 | } elseif ( is_singular() ) 169 | global $post; 170 | 171 | // get countable post types 172 | $post_types = $pvc->options['general']['post_types_count']; 173 | 174 | // break if display is not allowed 175 | if ( empty( $post_types ) || empty( $post ) || ! in_array( $post->post_type, $post_types, true ) ) 176 | return; 177 | 178 | if ( apply_filters( 'pvc_admin_display_post_views', true ) === false ) 179 | return; 180 | 181 | // add admin bar 182 | add_action( 'admin_bar_menu', [ $this, 'admin_bar_menu' ], 100 ); 183 | 184 | // backend 185 | if ( current_action() === 'admin_init' ) 186 | add_action( 'admin_head', [ $this, 'admin_bar_css' ] ); 187 | // frontend 188 | elseif ( current_action() === 'wp' ) 189 | add_action( 'wp_head', [ $this, 'admin_bar_css' ] ); 190 | } 191 | 192 | /** 193 | * Add admin CSS. 194 | * 195 | * @return void 196 | */ 197 | public function admin_bar_css() { 198 | $html = ' 199 | '; 218 | 219 | echo wp_kses( $html, [ 'style' => [] ] ); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /includes/class-admin.php: -------------------------------------------------------------------------------- 1 | [ 'POST' ], 55 | 'callback' => [ $this, 'block_editor_update_callback' ], 56 | 'permission_callback' => [ $this, 'check_rest_route_permissions' ], 57 | 'args' => [ 58 | 'id' => [ 59 | 'sanitize_callback' => 'absint', 60 | ] 61 | ] 62 | ] 63 | ); 64 | } 65 | 66 | /** 67 | * Check whether user has permissions to perform post views update in block editor. 68 | * 69 | * @param object $request WP_REST_Request 70 | * @return bool|WP_Error 71 | */ 72 | public function check_rest_route_permissions( $request ) { 73 | // break if current user can't edit this post 74 | if ( ! current_user_can( 'edit_post', (int) $request->get_param( 'id' ) ) ) 75 | return new WP_Error( 'pvc-user-not-allowed', __( 'You are not allowed to edit this item.', 'post-views-counter' ) ); 76 | 77 | // break if views editing is restricted 78 | if ( (bool) Post_Views_Counter()->options['display']['restrict_edit_views'] === true && ! current_user_can( apply_filters( 'pvc_restrict_edit_capability', 'manage_options' ) ) ) 79 | return new WP_Error( 'pvc-user-not-allowed', __( 'You are not allowed to edit this item.', 'post-views-counter' ) ); 80 | 81 | return true; 82 | } 83 | 84 | /** 85 | * REST API callback for block editor endpoint. 86 | * 87 | * @param array $data 88 | * @return string|int 89 | */ 90 | public function block_editor_update_callback( $data ) { 91 | // get main instance 92 | $pvc = Post_Views_Counter(); 93 | 94 | // cast post ID 95 | $post_id = ! empty( $data['id'] ) ? (int) $data['id'] : 0; 96 | 97 | // cast post views 98 | $post_views = ! empty( $data['post_views'] ) ? (int) $data['post_views'] : 0; 99 | 100 | // get countable post types 101 | $post_types = $pvc->options['general']['post_types_count']; 102 | 103 | // check if post exists 104 | $post = get_post( $post_id ); 105 | 106 | // whether to count this post type or not 107 | if ( empty( $post_types ) || empty( $post ) || ! in_array( $post->post_type, $post_types, true ) ) 108 | return wp_send_json_error( __( 'Invalid post ID.', 'post-views-counter' ) ); 109 | 110 | // break if current user can't edit this post 111 | if ( ! current_user_can( 'edit_post', $post_id ) ) 112 | return wp_send_json_error( __( 'You are not allowed to edit this item.', 'post-views-counter' ) ); 113 | 114 | // break if views editing is restricted 115 | if ( (bool) $pvc->options['display']['restrict_edit_views'] === true && ! current_user_can( apply_filters( 'pvc_restrict_edit_capability', 'manage_options' ) ) ) 116 | return wp_send_json_error( __( 'You are not allowed to edit this item.', 'post-views-counter' ) ); 117 | 118 | // update post views 119 | pvc_update_post_views( $post_id, $post_views ); 120 | 121 | do_action( 'pvc_after_update_post_views_count', $post_id ); 122 | 123 | return $post_id; 124 | } 125 | 126 | /** 127 | * Enqueue frontend and editor JavaScript and CSS. 128 | * 129 | * @global string $pagenow 130 | * @global string $wp_version 131 | * 132 | * @return void 133 | */ 134 | public function block_editor_enqueue_scripts() { 135 | global $pagenow, $wp_version; 136 | 137 | // get main instance 138 | $pvc = Post_Views_Counter(); 139 | 140 | // skip widgets and customizer pages 141 | if ( $pagenow === 'widgets.php' || $pagenow === 'customize.php' ) 142 | return; 143 | 144 | // enqueue the bundled block JS file 145 | wp_enqueue_script( 'pvc-block-editor', POST_VIEWS_COUNTER_URL . '/js/block-editor.min.js', [ 'wp-element', 'wp-components', 'wp-edit-post', 'wp-data', 'wp-plugins' ], $pvc->defaults['version'] ); 146 | 147 | // restrict editing 148 | $can_edit = false; 149 | 150 | $restrict = (bool) $pvc->options['display']['restrict_edit_views']; 151 | 152 | if ( $restrict === false || ( $restrict === true && current_user_can( apply_filters( 'pvc_restrict_edit_capability', 'manage_options' ) ) ) ) 153 | $can_edit = true; 154 | 155 | // get total post views 156 | $id = get_the_ID(); 157 | $count = pvc_get_post_views( $id ); 158 | 159 | // disable views display and editing? 160 | if ( apply_filters( 'pvc_admin_display_views', true, $id ) === false ) { 161 | $count = '—'; 162 | $can_edit = false; 163 | } 164 | 165 | // prepare script data 166 | $script_data = [ 167 | 'postID' => $id, 168 | 'postViews' => $count, 169 | 'canEdit' => $can_edit, 170 | 'nonce' => wp_create_nonce( 'wp_rest' ), 171 | 'wpGreater53' => version_compare( $wp_version, '5.3', '>=' ), 172 | 'textPostViews' => esc_html__( 'Post Views', 'post-views-counter' ), 173 | 'textHelp' => esc_html__( 'Adjust the views count for this post.', 'post-views-counter' ), 174 | 'textCancel' => esc_html__( 'Cancel', 'post-views-counter' ) 175 | ]; 176 | 177 | wp_add_inline_script( 'pvc-block-editor', 'var pvcEditorArgs = ' . wp_json_encode( $script_data ) . ";\n", 'before' ); 178 | 179 | // enqueue frontend and editor block styles 180 | wp_enqueue_style( 'pvc-block-editor', POST_VIEWS_COUNTER_URL . '/css/block-editor.min.css', '', $pvc->defaults['version'] ); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /assets/microtip/microtip.min.css: -------------------------------------------------------------------------------- 1 | /* ------------------------------------------------------------------- 2 | Microtip 3 | 4 | Modern, lightweight css-only tooltips 5 | Just 1kb minified and gzipped 6 | 7 | @author Ghosh 8 | @package Microtip 9 | 10 | ---------------------------------------------------------------------- 11 | 1. Base Styles 12 | 2. Direction Modifiers 13 | 3. Position Modifiers 14 | --------------------------------------------------------------------*/ 15 | [aria-label][role~="tooltip"]{position:relative}[aria-label][role~="tooltip"]::before,[aria-label][role~="tooltip"]::after{transform:translate3d(0,0,0);-webkit-backface-visibility:hidden;backface-visibility:hidden;will-change:transform;opacity:0;pointer-events:none;transition:all var(--microtip-transition-duration,.18s) var(--microtip-transition-easing,ease-in-out) var(--microtip-transition-delay,0s);position:absolute;box-sizing:border-box;z-index:10;transform-origin:top}[aria-label][role~="tooltip"]::before{background-size:100% auto!important;content:""}[aria-label][role~="tooltip"]::after{background:rgba(17,17,17,.9);border-radius:4px;color:#fff;content:attr(aria-label);font-size:var(--microtip-font-size,13px);font-weight:var(--microtip-font-weight,normal);text-transform:var(--microtip-text-transform,none);padding:.5em 1em;white-space:nowrap;box-sizing:content-box}[aria-label][role~="tooltip"]:hover::before,[aria-label][role~="tooltip"]:hover::after,[aria-label][role~="tooltip"]:focus::before,[aria-label][role~="tooltip"]:focus::after{opacity:1;pointer-events:auto}[role~="tooltip"][data-microtip-position|="top"]::before{background:url(data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba%2817,%2017,%2017,%200.9%29%22%20transform%3D%22rotate%280%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E) no-repeat;height:6px;width:18px;margin-bottom:5px}[role~="tooltip"][data-microtip-position|="top"]::after{margin-bottom:11px}[role~="tooltip"][data-microtip-position|="top"]::before{transform:translate3d(-50%,0,0);bottom:100%;left:50%}[role~="tooltip"][data-microtip-position|="top"]:hover::before{transform:translate3d(-50%,-5px,0)}[role~="tooltip"][data-microtip-position|="top"]::after{transform:translate3d(-50%,0,0);bottom:100%;left:50%}[role~="tooltip"][data-microtip-position="top"]:hover::after{transform:translate3d(-50%,-5px,0)}[role~="tooltip"][data-microtip-position="top-left"]::after{transform:translate3d(calc(-100% + 16px),0,0);bottom:100%}[role~="tooltip"][data-microtip-position="top-left"]:hover::after{transform:translate3d(calc(-100% + 16px),-5px,0)}[role~="tooltip"][data-microtip-position="top-right"]::after{transform:translate3d(calc(0% + -16px),0,0);bottom:100%}[role~="tooltip"][data-microtip-position="top-right"]:hover::after{transform:translate3d(calc(0% + -16px),-5px,0)}[role~="tooltip"][data-microtip-position|="bottom"]::before{background:url(data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba%2817,%2017,%2017,%200.9%29%22%20transform%3D%22rotate%28180%2018%206%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E) no-repeat;height:6px;width:18px;margin-top:5px;margin-bottom:0}[role~="tooltip"][data-microtip-position|="bottom"]::after{margin-top:11px}[role~="tooltip"][data-microtip-position|="bottom"]::before{transform:translate3d(-50%,-10px,0);bottom:auto;left:50%;top:100%}[role~="tooltip"][data-microtip-position|="bottom"]:hover::before{transform:translate3d(-50%,0,0)}[role~="tooltip"][data-microtip-position|="bottom"]::after{transform:translate3d(-50%,-10px,0);top:100%;left:50%}[role~="tooltip"][data-microtip-position="bottom"]:hover::after{transform:translate3d(-50%,0,0)}[role~="tooltip"][data-microtip-position="bottom-left"]::after{transform:translate3d(calc(-100% + 16px),-10px,0);top:100%}[role~="tooltip"][data-microtip-position="bottom-left"]:hover::after{transform:translate3d(calc(-100% + 16px),0,0)}[role~="tooltip"][data-microtip-position="bottom-right"]::after{transform:translate3d(calc(0% + -16px),-10px,0);top:100%}[role~="tooltip"][data-microtip-position="bottom-right"]:hover::after{transform:translate3d(calc(0% + -16px),0,0)}[role~="tooltip"][data-microtip-position="left"]::before,[role~="tooltip"][data-microtip-position="left"]::after{bottom:auto;left:auto;right:100%;top:50%;transform:translate3d(10px,-50%,0)}[role~="tooltip"][data-microtip-position="left"]::before{background:url(data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba%2817,%2017,%2017,%200.9%29%22%20transform%3D%22rotate%28-90%2018%2018%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E) no-repeat;height:18px;width:6px;margin-right:5px;margin-bottom:0}[role~="tooltip"][data-microtip-position="left"]::after{margin-right:11px}[role~="tooltip"][data-microtip-position="left"]:hover::before,[role~="tooltip"][data-microtip-position="left"]:hover::after{transform:translate3d(0,-50%,0)}[role~="tooltip"][data-microtip-position="right"]::before,[role~="tooltip"][data-microtip-position="right"]::after{bottom:auto;left:100%;top:50%;transform:translate3d(-10px,-50%,0)}[role~="tooltip"][data-microtip-position="right"]::before{background:url(data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba%2817,%2017,%2017,%200.9%29%22%20transform%3D%22rotate%2890%206%206%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E) no-repeat;height:18px;width:6px;margin-bottom:0;margin-left:5px}[role~="tooltip"][data-microtip-position="right"]::after{margin-left:11px}[role~="tooltip"][data-microtip-position="right"]:hover::before,[role~="tooltip"][data-microtip-position="right"]:hover::after{transform:translate3d(0,-50%,0)}[role~="tooltip"][data-microtip-size="small"]::after{white-space:initial;width:80px}[role~="tooltip"][data-microtip-size="medium"]::after{white-space:initial;width:150px}[role~="tooltip"][data-microtip-size="large"]::after{white-space:initial;width:260px} -------------------------------------------------------------------------------- /src/block-editor.js: -------------------------------------------------------------------------------- 1 | // set initial variables 2 | const { Fragment, Component } = wp.element; 3 | const { withSelect } = wp.data; 4 | const { registerPlugin } = wp.plugins; 5 | const { TextControl, Button, Popover } = wp.components; 6 | const { PluginPostStatusInfo } = wp.editPost; 7 | 8 | // setup wrapper component 9 | class PostViews extends Component { 10 | constructor() { 11 | super( ...arguments ); 12 | 13 | this.state = { 14 | postViews: pvcEditorArgs.postViews, 15 | isVisible: false 16 | }; 17 | 18 | // bind to provide access to 'this' object 19 | this.handleClick = this.handleClick.bind( this ); 20 | this.handleClickOutside = this.handleClickOutside.bind( this ); 21 | this.handleCancel = this.handleCancel.bind( this ); 22 | this.handleSetViews = this.handleSetViews.bind( this ); 23 | } 24 | 25 | // show/hide popover on button click 26 | handleClick ( e ) { 27 | if ( e.target.classList.contains( 'edit-post-post-views-toggle-link' ) ) { 28 | this.setState( ( prevState ) => ( 29 | { isVisible: !prevState.isVisible } 30 | ) ); 31 | } 32 | } 33 | 34 | // show/hide popover on outside click 35 | handleClickOutside ( e ) { 36 | if ( !e.target.classList.contains( 'edit-post-post-views-toggle-link' ) ) { 37 | this.setState( ( prevState ) => ( 38 | { isVisible: !prevState.isVisible } 39 | ) ); 40 | } 41 | } 42 | 43 | // reset views on cancel click 44 | handleCancel ( e ) { 45 | this.setState( ( prevState ) => ( 46 | { 47 | postViews: pvcEditorArgs.postViews, 48 | isVisible: !prevState.isVisible 49 | } 50 | ) ); 51 | } 52 | 53 | // reset post views on change 54 | handleSetViews ( value ) { 55 | // force update button to be clickable 56 | wp.data.dispatch( 'core/editor' ).editPost( { meta: { _pvc_post_views: value } } ); 57 | 58 | this.setState( () => { 59 | return { 60 | postViews: value 61 | } 62 | } ); 63 | } 64 | 65 | // save the post views 66 | static getDerivedStateFromProps ( nextProps, state ) { 67 | // bail if autosave 68 | if ( ( nextProps.isPublishing || nextProps.isSaving ) && !nextProps.isAutoSaving ) { 69 | wp.apiRequest( { path: `/post-views-counter/update-post-views/?id=${nextProps.postId}`, method: 'POST', data: { post_views: state.postViews } } ).then( 70 | ( data ) => { 71 | return data; 72 | }, 73 | ( error ) => { 74 | return error; 75 | } 76 | ) 77 | } 78 | } 79 | 80 | render () { 81 | return ( 82 | 90 | ) 91 | } 92 | } 93 | 94 | // create child component 95 | const PostViewsComponent = ( props ) => { 96 | // render component 97 | return ( 98 | 99 | 100 |
101 | { pvcEditorArgs.textPostViews } 102 |
103 | { !pvcEditorArgs.canEdit && ( 104 |
105 |
106 | 114 |
115 |
116 | ) } 117 | { pvcEditorArgs.canEdit && ( 118 |
119 |
120 | 149 | 150 | ) : ( 151 | 156 | { pvcEditorArgs.textPostViews } 157 | 164 |

{ pvcEditorArgs.textHelp }

165 | 172 |
173 | ) ) } 174 | 175 |
176 |
177 | ) } 178 |
179 |
180 | ) 181 | } 182 | 183 | // get post data using withSelect higher-order component 184 | const Plugin = withSelect( ( select, { forceIsSaving } ) => { 185 | const { 186 | getCurrentPostId, 187 | isSavingPost, 188 | isPublishingPost, 189 | isAutosavingPost, 190 | } = select( 'core/editor' ); 191 | 192 | return { 193 | postId: getCurrentPostId(), 194 | isSaving: forceIsSaving || isSavingPost(), 195 | isAutoSaving: isAutosavingPost(), 196 | isPublishing: isPublishingPost(), 197 | }; 198 | } )( PostViews ) 199 | 200 | // register the plugin 201 | registerPlugin( 'post-views-counter', { 202 | icon: '', 203 | render: Plugin 204 | } ) -------------------------------------------------------------------------------- /assets/micromodal/micromodal.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).MicroModal=t()}(this,(function(){"use strict";function e(e,t){for(var o=0;oe.length)&&(t=e.length);for(var o=0,n=new Array(t);o0&&this.registerTriggers.apply(this,t(a)),this.onClick=this.onClick.bind(this),this.onKeydown=this.onKeydown.bind(this)}var i,a,r;return i=o,(a=[{key:"registerTriggers",value:function(){for(var e=this,t=arguments.length,o=new Array(t),n=0;n0&&void 0!==arguments[0]?arguments[0]:null;if(this.activeElement=document.activeElement,this.modal.setAttribute("aria-hidden","false"),this.modal.classList.add(this.config.openClass),this.scrollBehaviour("disable"),this.addEventListeners(),this.config.awaitOpenAnimation){var o=function t(){e.modal.removeEventListener("animationend",t,!1),e.setFocusToFirstNode()};this.modal.addEventListener("animationend",o,!1)}else this.setFocusToFirstNode();this.config.onShow(this.modal,this.activeElement,t)}},{key:"closeModal",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,t=this.modal;if(this.modal.setAttribute("aria-hidden","true"),this.removeEventListeners(),this.scrollBehaviour("enable"),this.activeElement&&this.activeElement.focus&&this.activeElement.focus(),this.config.onClose(this.modal,this.activeElement,e),this.config.awaitCloseAnimation){var o=this.config.openClass;this.modal.addEventListener("animationend",(function e(){t.classList.remove(o),t.removeEventListener("animationend",e,!1)}),!1)}else t.classList.remove(this.config.openClass)}},{key:"closeModalById",value:function(e){this.modal=document.getElementById(e),this.modal&&this.closeModal()}},{key:"scrollBehaviour",value:function(e){if(this.config.disableScroll){var t=document.querySelector("body");switch(e){case"enable":Object.assign(t.style,{overflow:""});break;case"disable":Object.assign(t.style,{overflow:"hidden"})}}}},{key:"addEventListeners",value:function(){this.modal.addEventListener("touchstart",this.onClick),this.modal.addEventListener("click",this.onClick),document.addEventListener("keydown",this.onKeydown)}},{key:"removeEventListeners",value:function(){this.modal.removeEventListener("touchstart",this.onClick),this.modal.removeEventListener("click",this.onClick),document.removeEventListener("keydown",this.onKeydown)}},{key:"onClick",value:function(e){(e.target.hasAttribute(this.config.closeTrigger)||e.target.parentNode.hasAttribute(this.config.closeTrigger))&&(e.preventDefault(),e.stopPropagation(),this.closeModal(e))}},{key:"onKeydown",value:function(e){27===e.keyCode&&this.closeModal(e),9===e.keyCode&&this.retainFocus(e)}},{key:"getFocusableNodes",value:function(){var e=this.modal.querySelectorAll(n);return Array.apply(void 0,t(e))}},{key:"setFocusToFirstNode",value:function(){var e=this;if(!this.config.disableFocus){var t=this.getFocusableNodes();if(0!==t.length){var o=t.filter((function(t){return!t.hasAttribute(e.config.closeTrigger)}));o.length>0&&o[0].focus(),0===o.length&&t[0].focus()}}}},{key:"retainFocus",value:function(e){var t=this.getFocusableNodes();if(0!==t.length)if(t=t.filter((function(e){return null!==e.offsetParent})),this.modal.contains(document.activeElement)){var o=t.indexOf(document.activeElement);e.shiftKey&&0===o&&(t[t.length-1].focus(),e.preventDefault()),!e.shiftKey&&t.length>0&&o===t.length-1&&(t[0].focus(),e.preventDefault())}else t[0].focus()}}])&&e(i.prototype,a),r&&e(i,r),o}(),a=null,r=function(e){if(!document.getElementById(e))return console.warn("MicroModal: ❗Seems like you have missed %c'".concat(e,"'"),"background-color: #f8f9fa;color: #50596c;font-weight: bold;","ID somewhere in your code. Refer example below to resolve it."),console.warn("%cExample:","background-color: #f8f9fa;color: #50596c;font-weight: bold;",'')),!1},s=function(e,t){if(function(e){e.length<=0&&(console.warn("MicroModal: ❗Please specify at least one %c'micromodal-trigger'","background-color: #f8f9fa;color: #50596c;font-weight: bold;","data attribute."),console.warn("%cExample:","background-color: #f8f9fa;color: #50596c;font-weight: bold;",''))}(e),!t)return!0;for(var o in t)r(o);return!0},{init:function(e){var o=Object.assign({},{openTrigger:"data-micromodal-trigger"},e),n=t(document.querySelectorAll("[".concat(o.openTrigger,"]"))),r=function(e,t){var o=[];return e.forEach((function(e){var n=e.attributes[t].value;void 0===o[n]&&(o[n]=[]),o[n].push(e)})),o}(n,o.openTrigger);if(!0!==o.debugMode||!1!==s(n,r))for(var l in r){var c=r[l];o.targetModal=l,o.triggers=t(c),a=new i(o)}},show:function(e,t){var o=t||{};o.targetModal=e,!0===o.debugMode&&!1===r(e)||(a&&a.removeEventListeners(),(a=new i(o)).showModal())},close:function(e){e?a.closeModalById(e):a.closeModal()}});return"undefined"!=typeof window&&(window.MicroModal=l),l})); 2 | -------------------------------------------------------------------------------- /includes/class-frontend.php: -------------------------------------------------------------------------------- 1 | get_the_ID(), 48 | 'type' => 'post' 49 | ]; 50 | 51 | // combine attributes 52 | $atts = apply_filters( 'pvc_post_views_shortcode_atts', shortcode_atts( $defaults, $args ) ); 53 | 54 | // default type? 55 | if ( $atts['type'] === 'post' ) 56 | $views = function_exists( 'pvc_post_views' ) ? pvc_post_views( $atts['id'], false ) : 0; 57 | 58 | return apply_filters( 'pvc_post_views_shortcode', $views, $atts ); 59 | } 60 | 61 | /** 62 | * Display number of post views. 63 | * 64 | * @return void 65 | */ 66 | public function run() { 67 | if ( is_admin() && ! wp_doing_ajax() ) 68 | return; 69 | 70 | $filter = apply_filters( 'pvc_shortcode_filter_hook', Post_Views_Counter()->options['display']['position'] ); 71 | 72 | // valid filter? 73 | if ( ! empty( $filter ) && in_array( $filter, [ 'before', 'after' ] ) ) { 74 | // post content 75 | add_filter( 'the_content', [ $this, 'add_post_views_count' ] ); 76 | 77 | // bbpress support 78 | add_action( 'bbp_template_' . $filter . '_single_topic', [ $this, 'display_bbpress_post_views' ] ); 79 | add_action( 'bbp_template_' . $filter . '_single_forum', [ $this, 'display_bbpress_post_views' ] ); 80 | // custom 81 | } elseif ( $filter !== 'manual' && is_string( $filter ) ) 82 | add_filter( $filter, [ $this, 'add_post_views_count' ] ); 83 | } 84 | 85 | /** 86 | * Add post views counter to forum/topic of bbPress. 87 | * 88 | * @return void 89 | */ 90 | public function display_bbpress_post_views() { 91 | $post_id = get_the_ID(); 92 | 93 | // check only for forums and topics 94 | if ( bbp_is_forum( $post_id ) || bbp_is_topic( $post_id ) ) 95 | echo $this->add_post_views_count( '' ); 96 | } 97 | 98 | /** 99 | * Add post views counter to content. 100 | * 101 | * @param string $content 102 | * @return string 103 | */ 104 | public function add_post_views_count( $content = '' ) { 105 | // get main instance 106 | $pvc = Post_Views_Counter(); 107 | 108 | $display = false; 109 | 110 | // post type check 111 | if ( ! empty( $pvc->options['display']['post_types_display'] ) ) 112 | $display = is_singular( $pvc->options['display']['post_types_display'] ); 113 | 114 | // page visibility check 115 | if ( ! empty( $pvc->options['display']['page_types_display'] ) ) { 116 | foreach ( $pvc->options['display']['page_types_display'] as $page ) { 117 | switch ( $page ) { 118 | case 'singular': 119 | if ( is_singular( $pvc->options['display']['post_types_display'] ) ) 120 | $display = true; 121 | break; 122 | 123 | case 'archive': 124 | if ( is_archive() ) 125 | $display = true; 126 | break; 127 | 128 | case 'search': 129 | if ( is_search() ) 130 | $display = true; 131 | break; 132 | 133 | case 'home': 134 | if ( is_home() || is_front_page() ) 135 | $display = true; 136 | break; 137 | } 138 | } 139 | } 140 | 141 | // get groups to check it faster 142 | $groups = $pvc->options['display']['restrict_display']['groups']; 143 | 144 | // whether to display views 145 | if ( is_user_logged_in() ) { 146 | // exclude logged in users? 147 | if ( in_array( 'users', $groups, true ) ) 148 | $display = false; 149 | // exclude specific roles? 150 | elseif ( in_array( 'roles', $groups, true ) && $pvc->counter->is_user_role_excluded( get_current_user_id(), $pvc->options['display']['restrict_display']['roles'] ) ) 151 | $display = false; 152 | // exclude guests? 153 | } elseif ( in_array( 'guests', $groups, true ) ) 154 | $display = false; 155 | 156 | // we don't want to mess custom loops 157 | if ( ! in_the_loop() && ! class_exists( 'bbPress' ) ) 158 | $display = false; 159 | 160 | if ( (bool) apply_filters( 'pvc_display_views_count', $display ) === true ) { 161 | $filter = apply_filters( 'pvc_shortcode_filter_hook', $pvc->options['display']['position'] ); 162 | 163 | switch ( $filter ) { 164 | case 'after': 165 | $content = $content . do_shortcode( '[post-views]' ); 166 | break; 167 | 168 | case 'before': 169 | $content = do_shortcode( '[post-views]' ) . $content; 170 | break; 171 | 172 | case 'manual': 173 | default: 174 | break; 175 | } 176 | } 177 | 178 | return $content; 179 | } 180 | 181 | /** 182 | * Get frontend script arguments. 183 | * 184 | * @return array 185 | */ 186 | public function get_frontend_script_args() { 187 | return $this->script_args; 188 | } 189 | 190 | /** 191 | * Enqueue frontend scripts and styles. 192 | * 193 | * @return void 194 | */ 195 | public function wp_enqueue_scripts() { 196 | // get main instance 197 | $pvc = Post_Views_Counter(); 198 | 199 | // enable styles? 200 | if ( (bool) apply_filters( 'pvc_enqueue_styles', true ) === true ) { 201 | // load dashicons 202 | wp_enqueue_style( 'dashicons' ); 203 | 204 | // load style 205 | wp_enqueue_style( 'post-views-counter-frontend', POST_VIEWS_COUNTER_URL . '/css/frontend' . ( ! ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '.min' : '' ) . '.css', [], $pvc->defaults['version'] ); 206 | } 207 | 208 | // skip special requests 209 | if ( is_preview() || is_feed() || is_trackback() || ( function_exists( 'is_favicon' ) && is_favicon() ) || is_customize_preview() ) 210 | return; 211 | 212 | // get countable post types 213 | $post_types = $pvc->options['general']['post_types_count']; 214 | 215 | // whether to count this post type or not 216 | if ( empty( $post_types ) || ! is_singular( $post_types ) ) 217 | return; 218 | 219 | // get current post id 220 | $post_id = (int) get_the_ID(); 221 | 222 | // allow to run check post? 223 | if ( ! (bool) apply_filters( 'pvc_run_check_post', true, $post_id ) ) 224 | return; 225 | 226 | // get counter mode 227 | $mode = $pvc->options['general']['counter_mode']; 228 | 229 | // specific counter mode? 230 | if ( in_array( $mode, [ 'js', 'rest_api' ], true ) ) { 231 | wp_enqueue_script( 'post-views-counter-frontend', POST_VIEWS_COUNTER_URL . '/js/frontend' . ( ! ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '.min' : '' ) . '.js', [], $pvc->defaults['version'], false ); 232 | 233 | // prepare args 234 | $args = [ 235 | 'mode' => $mode, 236 | 'postID' => $post_id, 237 | 'requestURL' => '', 238 | 'nonce' => '', 239 | 'dataStorage' => $pvc->options['general']['data_storage'], 240 | 'multisite' => ( is_multisite() ? (int) get_current_blog_id() : false ), 241 | 'path' => empty( COOKIEPATH ) || ! is_string( COOKIEPATH ) ? '/' : COOKIEPATH, 242 | 'domain' => empty( COOKIE_DOMAIN ) || ! is_string( COOKIE_DOMAIN ) ? '' : COOKIE_DOMAIN 243 | ]; 244 | 245 | switch ( $mode ) { 246 | // rest api 247 | case 'rest_api': 248 | $args['requestURL'] = rest_url( 'post-views-counter/view-post/' . $args['postID'] ); 249 | $args['nonce'] = wp_create_nonce( 'wp_rest' ); 250 | break; 251 | 252 | // javascript 253 | case 'js': 254 | default: 255 | $args['requestURL'] = admin_url( 'admin-ajax.php' ); 256 | $args['nonce'] = wp_create_nonce( 'pvc-check-post' ); 257 | } 258 | 259 | // make it safe 260 | $args['requestURL'] = esc_url_raw( $args['requestURL'] ); 261 | 262 | // set script args 263 | $this->script_args = apply_filters( 'pvc_frontend_script_args', $args, 'standard' ); 264 | 265 | wp_add_inline_script( 'post-views-counter-frontend', 'var pvcArgsFrontend = ' . wp_json_encode( $this->script_args ) . ";\n", 'before' ); 266 | } 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /js/column-modal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Post Views Counter - Column Modal Chart 3 | */ 4 | ( function( $ ) { 5 | 'use strict'; 6 | 7 | // check if jQuery is available 8 | if ( typeof $ === 'undefined' ) 9 | return; 10 | 11 | let pvcModalChart = null; 12 | let currentPostId = null; 13 | 14 | // check if localized data is available 15 | if ( typeof pvcColumnModal === 'undefined' ) 16 | return; 17 | 18 | /** 19 | * Initialize Micromodal 20 | */ 21 | function initMicromodal() { 22 | if ( typeof MicroModal === 'undefined' ) 23 | return false; 24 | 25 | // initialize with basic config 26 | MicroModal.init( { 27 | disableScroll: true, 28 | awaitCloseAnimation: true 29 | } ); 30 | 31 | return true; 32 | } 33 | 34 | /** 35 | * Prepare modal for specific post 36 | */ 37 | function prepareModalForPost( postId, postTitle ) { 38 | if ( ! postId ) 39 | return false; 40 | 41 | currentPostId = postId; 42 | 43 | // set modal title 44 | $( '#pvc-modal-title' ).text( postTitle ); 45 | 46 | // show loading state 47 | const $container = $( '.pvc-modal-chart-container' ); 48 | $container.addClass( 'loading' ); 49 | $container.find( '.spinner' ).addClass( 'is-active' ); 50 | 51 | // clear previous content 52 | $( '.pvc-modal-views-label' ).text( '' ); 53 | $( '.pvc-modal-count' ).text( '' ); 54 | $( '.pvc-modal-dates' ).html( '' ); 55 | 56 | return true; 57 | } 58 | 59 | function resetModalContent() { 60 | $( '#pvc-modal-title' ).text( '' ); 61 | $( '.pvc-modal-views-label' ).text( '' ); 62 | $( '.pvc-modal-count' ).text( '' ); 63 | $( '.pvc-modal-dates' ).html( '' ); 64 | 65 | const $container = $( '.pvc-modal-chart-container' ); 66 | $container.removeClass( 'loading' ); 67 | $container.find( '.spinner' ).removeClass( 'is-active' ); 68 | 69 | // remove any error messages 70 | $( '.pvc-modal-error' ).remove(); 71 | } 72 | 73 | /** 74 | * Load chart data via AJAX 75 | */ 76 | function loadChartData( postId, period ) { 77 | const $container = $( '.pvc-modal-chart-container' ); 78 | 79 | // show loading 80 | $container.addClass( 'loading' ); 81 | $container.find( '.spinner' ).addClass( 'is-active' ); 82 | 83 | $.ajax( { 84 | url: pvcColumnModal.ajaxURL, 85 | type: 'POST', 86 | dataType: 'json', 87 | data: { 88 | action: 'pvc_column_chart', 89 | nonce: pvcColumnModal.nonce, 90 | post_id: postId, 91 | period: period 92 | }, 93 | success: function( response ) { 94 | if ( response.success ) { 95 | renderChart( response.data ); 96 | } else { 97 | showError( response.data.message || pvcColumnModal.i18n.error ); 98 | } 99 | }, 100 | error: function( xhr, status, error ) { 101 | showError( pvcColumnModal.i18n.error ); 102 | }, 103 | complete: function() { 104 | $container.removeClass( 'loading' ); 105 | $container.find( '.spinner' ).removeClass( 'is-active' ); 106 | } 107 | } ); 108 | } 109 | 110 | /** 111 | * Render chart with data 112 | */ 113 | function renderChart( data ) { 114 | const ctx = document.getElementById( 'pvc-modal-chart' ); 115 | 116 | if ( ! ctx ) 117 | return; 118 | 119 | // destroy existing chart 120 | if ( pvcModalChart ) { 121 | pvcModalChart.destroy(); 122 | pvcModalChart = null; 123 | } 124 | 125 | // remove any error messages and show canvas 126 | $( '.pvc-modal-error' ).remove(); 127 | $( ctx ).show(); 128 | 129 | // update stats 130 | $( '.pvc-modal-views-label' ).text( pvcColumnModal.i18n.summary ); 131 | $( '.pvc-modal-count' ).text( data.total_views.toLocaleString() ); 132 | 133 | // update date navigation 134 | $( '.pvc-modal-dates' ).html( data.dates_html ); 135 | 136 | /* debug logging (admins only) - shows views keys returned from AJAX 137 | if ( typeof console !== 'undefined' && data.debug_views_keys ) { 138 | console.debug( 'PVC debug: views keys for post ' + data.post_id + ':', data.debug_views_keys ); 139 | console.debug( 'PVC debug: views sample by keys:', data.debug_views ); 140 | console.debug( 'PVC debug: example date_key used by client:', data.debug_date_key_example ); 141 | } 142 | 143 | if ( typeof console !== 'undefined' && data.data ) { 144 | console.debug( 'PVC debug: chart data object', data.data ); 145 | console.debug( 'PVC debug: dataset length', data.data.datasets && data.data.datasets[0] ? data.data.datasets[0].data.length : 0 ); 146 | } 147 | */ 148 | 149 | // chart configuration 150 | const config = { 151 | type: 'line', 152 | data: data.data, 153 | options: { 154 | maintainAspectRatio: false, 155 | responsive: true, 156 | plugins: { 157 | legend: { 158 | display: false 159 | }, 160 | tooltip: { 161 | callbacks: { 162 | title: function( context ) { 163 | return data.data.dates[context[0].dataIndex]; 164 | }, 165 | label: function( context ) { 166 | const count = context.parsed.y; 167 | const viewText = count === 1 ? pvcColumnModal.i18n.view : pvcColumnModal.i18n.views; 168 | return count.toLocaleString() + ' ' + viewText; 169 | } 170 | } 171 | } 172 | }, 173 | scales: { 174 | x: { 175 | display: true, 176 | grid: { 177 | display: false 178 | } 179 | }, 180 | y: { 181 | display: true, 182 | beginAtZero: true, 183 | ticks: { 184 | precision: 0 185 | }, 186 | grid: { 187 | color: 'rgba(0, 0, 0, 0.05)' 188 | } 189 | } 190 | } 191 | } 192 | }; 193 | 194 | // apply design from server 195 | if ( data.design ) { 196 | config.data.datasets.forEach( function( dataset ) { 197 | Object.assign( dataset, data.design ); 198 | dataset.tension = 0.4; // match dashboard curve 199 | } ); 200 | } 201 | 202 | // create chart 203 | pvcModalChart = new Chart( ctx.getContext( '2d' ), config ); 204 | } 205 | 206 | /** 207 | * Show error message without destroying modal structure 208 | */ 209 | function showError( message ) { 210 | // destroy existing chart 211 | if ( pvcModalChart ) { 212 | pvcModalChart.destroy(); 213 | pvcModalChart = null; 214 | } 215 | 216 | // reset states 217 | $( '.pvc-modal-summary' ).text( '' ); 218 | $( '.pvc-modal-dates' ).html( '' ); 219 | 220 | const $container = $( '.pvc-modal-chart-container' ); 221 | $container.removeClass( 'loading' ); 222 | $container.find( '.spinner' ).removeClass( 'is-active' ); 223 | 224 | // remove any existing error message 225 | $( '.pvc-modal-error' ).remove(); 226 | 227 | // inject error message before chart container 228 | $container.before( '

' + message + '

' ); 229 | 230 | // hide canvas during error 231 | $container.find( 'canvas' ).hide(); 232 | } 233 | 234 | /** 235 | * Document ready 236 | */ 237 | $( function() { 238 | // check if modal HTML exists 239 | if ( $( '#pvc-chart-modal' ).length === 0 ) 240 | return; 241 | 242 | // initialize Micromodal 243 | if ( ! initMicromodal() ) 244 | return; 245 | 246 | // handle click on view chart link 247 | $( document ).on( 'click', '.pvc-view-chart', function( e ) { 248 | e.preventDefault(); 249 | 250 | const postId = $( this ).data( 'post-id' ); 251 | const postTitle = $( this ).data( 'post-title' ); 252 | 253 | if ( ! postId ) 254 | return; 255 | 256 | // prepare modal with post data 257 | if ( prepareModalForPost( postId, postTitle ) ) { 258 | // open modal using MicroModal with callbacks 259 | if ( typeof MicroModal !== 'undefined' ) { 260 | MicroModal.show( 'pvc-chart-modal', { 261 | onShow: function( modal ) { 262 | if ( currentPostId ) 263 | loadChartData( currentPostId, '' ); 264 | }, 265 | onClose: function() { 266 | // destroy chart when modal closes 267 | if ( pvcModalChart ) { 268 | pvcModalChart.destroy(); 269 | pvcModalChart = null; 270 | } 271 | 272 | currentPostId = null; 273 | 274 | // reset modal content 275 | resetModalContent(); 276 | }, 277 | disableScroll: true, 278 | awaitCloseAnimation: true 279 | } ); 280 | } 281 | } 282 | } ); 283 | 284 | // handle period navigation 285 | $( document ).on( 'click', '.pvc-modal-nav-prev, .pvc-modal-nav-next', function( e ) { 286 | e.preventDefault(); 287 | 288 | // check if disabled 289 | if ( $( this ).hasClass( 'pvc-disabled' ) ) 290 | return; 291 | 292 | const period = $( this ).data( 'period' ); 293 | 294 | if ( period && currentPostId ) { 295 | loadChartData( currentPostId, period ); 296 | } 297 | } ); 298 | } ); 299 | 300 | } )( jQuery ); 301 | -------------------------------------------------------------------------------- /css/admin.css: -------------------------------------------------------------------------------- 1 | /* Post Views Counter settings */ 2 | .post-views-counter-settings.has-sidebar { 3 | display: flex; 4 | flex-direction: row; 5 | gap: 30px; 6 | justify-content: space-between; 7 | } 8 | .post-views-counter-settings form { 9 | min-width: 463px; 10 | width: auto; 11 | position: relative; 12 | } 13 | 14 | .post-views-sidebar { 15 | width: 280px; 16 | min-width: 280px; 17 | margin: 15px 0; 18 | position: relative; 19 | order: 1; 20 | } 21 | 22 | .post-views-sidebar .inner { 23 | padding: 2em; 24 | } 25 | 26 | .post-views-sidebar>div:not(:last-child) { 27 | margin-bottom: 3em; 28 | } 29 | 30 | .post-views-sidebar .inner img { 31 | max-width: 80%; 32 | height: auto; 33 | display: block; 34 | margin: 20px auto; 35 | } 36 | .post-views-counter-settings p.help, .post-views-counter-settings p.description { 37 | font-size: 13px; 38 | font-style: italic; 39 | line-height: 1.6; 40 | } 41 | .post-views-counter-settings div.ip-box { 42 | margin-bottom: 3px; 43 | } 44 | .post-views-counter-settings select { 45 | vertical-align: top; 46 | } 47 | .post-views-counter-settings .available { 48 | color: #00a32a; 49 | } 50 | .post-views-counter-settings .unavailable { 51 | color: #d63638; 52 | } 53 | .pvc-status-table .pvc-status { 54 | display: inline-block; 55 | font-size: 0.95em; 56 | } 57 | .pvc-status-table .pvc-status-active { 58 | color: #00a32a; 59 | } 60 | .pvc-status-table .pvc-status-missing { 61 | color: #d63638; 62 | } 63 | .pvc-subfield{ 64 | margin-top: 12px; 65 | } 66 | 67 | /* Single post edit screen */ 68 | #misc-publishing-actions #post-views #post-views-display:before { 69 | display: inline-block; 70 | font: 400 20px/1 dashicons; 71 | left: -1px; 72 | padding: 0 2px 0 0; 73 | position: relative; 74 | text-decoration: none !important; 75 | vertical-align: top; 76 | color: #888; 77 | content: "\f185"; 78 | top: -1px; 79 | } 80 | 81 | /* Listing edit screen */ 82 | .edit-php .widefat th#post_views { 83 | width: 5.5em; 84 | } 85 | .edit-php .widefat th.column-post_views .dashicons, 86 | .edit-php .widefat th.column-post_views .dashicons:before { 87 | font-size: 1.1em; 88 | vertical-align: middle; 89 | } 90 | .edit-php .widefat th .dash-title, .upload-php .widefat th .dash-title { 91 | display:none; 92 | } 93 | .edit-php .metabox-prefs .dash-icon { 94 | display:none; 95 | } 96 | .edit-php .widefat td .dashicons, 97 | .edit-php .widefat td .dashicons:before { 98 | font-size: 1.1em; 99 | } 100 | .edit-php #inline-edit-post_views input { 101 | width: auto; 102 | } 103 | 104 | .is-hidden { 105 | display:none !important; 106 | visibility:hidden !important 107 | } 108 | output { 109 | display: block; 110 | font-size: 30px; 111 | font-weight: bold; 112 | text-align: center; 113 | margin: 30px 0; 114 | width: 100%; 115 | } 116 | 117 | .post-views-credits { 118 | background: #fff; 119 | box-shadow: 0 0 0 1px rgba(0,0,0,0.05); 120 | } 121 | 122 | .post-views-credits .inner { 123 | text-align: center; 124 | margin: 0; 125 | } 126 | 127 | .post-views-counter-settings .pvc-button { 128 | color: #fff; 129 | background-color: #6610f2; 130 | border-color: #6610f2; 131 | } 132 | 133 | .post-views-counter-settings .pvc-button:active, 134 | .post-views-counter-settings .pvc-button:focus, 135 | .post-views-counter-settings .pvc-button:hover { 136 | color: #fff; 137 | background-color: #570ece; 138 | border-color: #570ece; 139 | } 140 | 141 | .post-views-counter-settings .pvc-button:focus { 142 | box-shadow: 0 0 0 1px #fff,0 0 0 3px #6610f2; 143 | } 144 | 145 | .post-views-credits h2 { 146 | border: none; 147 | padding-bottom: 0; 148 | font-size: 23px; 149 | font-weight: normal; 150 | margin: 0.25em 0 0.5em; 151 | color: #6610f2; 152 | } 153 | 154 | .post-views-credits h3 { 155 | font-size: 18px; 156 | line-height: 1.4; 157 | font-weight: normal; 158 | margin: 0; 159 | padding: 0; 160 | color: #6610f2; 161 | } 162 | 163 | .post-views-credits p:first-child { 164 | margin-top: 0; 165 | } 166 | 167 | .post-views-credits .pvc-sidebar-title { 168 | font-size: 17px; 169 | font-weight: bold; 170 | margin: 10px 0 20px; 171 | } 172 | 173 | .post-views-credits .pvc-sidebar-body { 174 | padding-bottom: 0; 175 | font-size: 14px; 176 | text-align: left; 177 | margin: 2em 0; 178 | padding: 0; 179 | } 180 | 181 | .post-views-credits .pvc-sidebar-footer { 182 | margin: 1em 0; 183 | } 184 | 185 | .post-views-credits .pvc-sidebar-body p { 186 | padding-left: 20px; 187 | margin: 0.75em 0; 188 | position: relative; 189 | } 190 | 191 | .post-views-credits .pvc-sidebar-body b { 192 | font-weight: bold; 193 | color: #000; 194 | } 195 | 196 | .post-views-credits .pvc-sidebar-body .pvc-icon { 197 | position: absolute; 198 | top: 0; 199 | left: 0; 200 | } 201 | 202 | .post-views-credits .pvc-sidebar-body .pvc-icon-check { 203 | box-sizing: border-box; 204 | display: block; 205 | transform: scale(1); 206 | width: 16px; 207 | height: 22px; 208 | border-radius: 100px; 209 | } 210 | 211 | .post-views-credits .pvc-sidebar-body .pvc-icon-check::after { 212 | content: ""; 213 | display: block; 214 | box-sizing: border-box; 215 | position: absolute; 216 | left: 0; 217 | top: 0; 218 | width: 6px; 219 | height: 10px; 220 | border-width: 0 2px 2px 0; 221 | border-style: solid; 222 | transform-origin: bottom left; 223 | transform: rotate(45deg); 224 | } 225 | 226 | #post_views_counter_other_license_setting .pvc-status-icon { 227 | vertical-align: middle; 228 | margin-left: 8px; 229 | padding-bottom: 3px; 230 | } 231 | 232 | #post_views_counter_other_license_setting .pvc-status-icon:before { 233 | content: "✗"; 234 | color: #d63638; 235 | } 236 | 237 | #post_views_counter_other_license_setting.license-status .pvc-status-icon:before { 238 | content: "✗"; 239 | color: #d63638; 240 | } 241 | 242 | #post_views_counter_other_license_setting.license-status.valid .pvc-status-icon:before { 243 | content: "✓"; 244 | color: #00a32a; 245 | } 246 | 247 | #pvc-reports-upgrade { 248 | position: absolute; 249 | left: 0; 250 | top: 0; 251 | height: 100%; 252 | width: 100%; 253 | overflow: hidden; 254 | box-sizing: border-box; 255 | min-height: 400px; 256 | } 257 | 258 | #pvc-reports-bg { 259 | width: 100%; 260 | height: auto; 261 | opacity: 0.8; 262 | filter: blur(2px); 263 | } 264 | 265 | #pvc-reports-modal { 266 | position: absolute; 267 | top: 50%; 268 | left: 50%; 269 | transform: translate(-50%, -50%); 270 | padding: 1.5em 3em; 271 | box-shadow: 0 0 25px 10px rgba(0,0,0,0.1); 272 | border-radius: 3px; 273 | background-color: #fff; 274 | text-align: center; 275 | width: 26em; 276 | } 277 | 278 | #pvc-reports-modal p { 279 | margin: 0; 280 | } 281 | 282 | #pvc-reports-modal h2 { 283 | font-size: 21px; 284 | font-weight: 400; 285 | margin: 0 0 10px 0; 286 | padding: 9px 0 4px; 287 | line-height: 1.3; 288 | } 289 | 290 | #pvc-reports-modal .button { 291 | margin-top: 25px; 292 | margin-bottom: 10px; 293 | } 294 | 295 | /* All Mobile Sizes (devices and browser) */ 296 | @media only screen and (max-width: 960px) { 297 | .post-views-counter-settings { 298 | flex-wrap: wrap; 299 | } 300 | .post-views-counter-settings .post-views-sidebar { 301 | width: 100%; 302 | } 303 | } 304 | 305 | /* Import Providers */ 306 | .pvc-provider-radio { 307 | display: inline-block; 308 | margin-right: 20px; 309 | font-weight: normal; 310 | } 311 | 312 | .pvc-provider-radio input[type="radio"] { 313 | margin-right: 5px; 314 | } 315 | 316 | .pvc-provider-disabled { 317 | opacity: 0.6; 318 | cursor: not-allowed; 319 | } 320 | 321 | .pvc-provider-disabled input[type="radio"] { 322 | cursor: not-allowed; 323 | } 324 | 325 | .pvc-provider-content { 326 | margin: 0; 327 | } 328 | 329 | .pvc-provider-fields { 330 | padding: 0; 331 | margin-top: 15px; 332 | } 333 | 334 | .pvc-provider-fields label { 335 | font-weight: 600; 336 | margin-bottom: 5px; 337 | display: block; 338 | } 339 | 340 | .pvc-provider-fields input.regular-text { 341 | margin-top: 5px; 342 | } 343 | 344 | .pvc-provider-unavailable { 345 | color: #d63638; 346 | font-style: italic; 347 | } 348 | 349 | .pvc-import-strategy { 350 | margin-bottom: 25px; 351 | } 352 | 353 | .post-views-counter-settings tr.pvc-pro-extended label[for="pvc_import_strategy_skip_existing"]:after, 354 | .post-views-counter-settings tr.pvc-pro-extended label[for="pvc_import_strategy_keep_higher_count"]:after, 355 | .post-views-counter-settings tr.pvc-pro-extended label[for="pvc_import_strategy_fill_empty_only"]:after { 356 | content: 'PRO'; 357 | display: inline-block; 358 | margin-left: 6px; 359 | padding: 1px 4px; 360 | font-size: 11px; 361 | border-radius: 4px; 362 | background-color: #ffc107; 363 | color: #fff; 364 | font-weight: 600; 365 | } 366 | 367 | .pvc-field-group label { 368 | margin-right: 8px; 369 | } 370 | 371 | .pvc-radio-vertical label { 372 | display: block; 373 | margin: 6px 0; 374 | font-weight: normal; 375 | } 376 | 377 | .pvc-radio-vertical input[type="radio"] { 378 | margin-right: 5px; 379 | } 380 | 381 | .pvc-import-actions { 382 | padding-top: 10px; 383 | } 384 | 385 | .pvc-import-actions .button { 386 | margin-right: 10px; 387 | } 388 | 389 | .pvc-import-actions .button-primary { 390 | font-weight: 600; 391 | } 392 | -------------------------------------------------------------------------------- /includes/class-update.php: -------------------------------------------------------------------------------- 1 | options['general']; 42 | 43 | if ( $general['reset_counts']['number'] > 0 ) { 44 | // unsupported data reset in minutes/hours 45 | if ( in_array( $general['reset_counts']['type'], [ 'minutes', 'hours' ], true ) ) { 46 | // set type to date 47 | $general['reset_counts']['type'] = 'days'; 48 | 49 | // new number of days 50 | if ( $general['reset_counts']['type'] === 'minutes' ) 51 | $general['reset_counts']['number'] = $general['reset_counts']['number'] * MINUTE_IN_SECONDS; 52 | else 53 | $general['reset_counts']['number'] = $general['reset_counts']['number'] * HOUR_IN_SECONDS; 54 | 55 | // how many days? 56 | $general['reset_counts']['number'] = (int) round( ceil( $general['reset_counts']['number'] / DAY_IN_SECONDS ) ); 57 | 58 | // force cron to update 59 | $general['cron_run'] = true; 60 | $general['cron_update'] = true; 61 | 62 | // update settings 63 | update_option( 'post_views_counter_settings_general', $general ); 64 | 65 | // update general options 66 | $pvc->options['general'] = $general; 67 | } 68 | 69 | // update cron job for all users 70 | $pvc->cron->check_cron(); 71 | } 72 | } 73 | 74 | // update 1.3.13+ 75 | if ( version_compare( $current_db_version, '1.3.13', '<=' ) ) { 76 | // get general options 77 | $general = $pvc->options['general']; 78 | 79 | // disable strict counts 80 | $general['strict_counts'] = false; 81 | 82 | // get default other options 83 | $other_options = $pvc->defaults['other']; 84 | 85 | // set current options 86 | $other_options['deactivation_delete'] = isset( $general['deactivation_delete'] ) ? (bool) $general['deactivation_delete'] : false; 87 | 88 | // add other options 89 | add_option( 'post_views_counter_settings_other', $other_options, null, false ); 90 | 91 | // update other options 92 | $pvc->options['other'] = $other_options; 93 | 94 | // remove old setting 95 | unset( $general['deactivation_delete'] ); 96 | 97 | // flush cache enabled? 98 | if ( $general['flush_interval']['number'] > 0 ) { 99 | if ( $pvc->counter->using_object_cache( true ) ) { 100 | // flush data from cache 101 | $pvc->counter->flush_cache_to_db(); 102 | } 103 | 104 | // unschedule cron event 105 | wp_clear_scheduled_hook( 'pvc_flush_cached_counts' ); 106 | 107 | // disable cache 108 | $general['flush_interval'] = [ 109 | 'number' => 0, 110 | 'type' => 'minutes' 111 | ]; 112 | } 113 | 114 | // update general options 115 | $pvc->options['general'] = $general; 116 | 117 | // update general options 118 | update_option( 'post_views_counter_settings_general', $general ); 119 | } 120 | 121 | // update 1.5.2+ 122 | if ( version_compare( $current_db_version, '1.5.2', '<=' ) ) { 123 | // get options 124 | $general = $pvc->options['general']; 125 | $display = $pvc->options['display']; 126 | 127 | // copy values 128 | $display['post_views_column'] = $general['post_views_column']; 129 | $display['restrict_edit_views'] = $general['restrict_edit_views']; 130 | 131 | // remove old values 132 | unset( $general['post_views_column'] ); 133 | unset( $general['restrict_edit_views'] ); 134 | 135 | // update settings 136 | update_option( 'post_views_counter_settings_general', $general ); 137 | update_option( 'post_views_counter_settings_display', $display ); 138 | 139 | // update options 140 | $pvc->options['general'] = $general; 141 | $pvc->options['display'] = $display; 142 | } 143 | 144 | // update 1.6.0+ - migrate import settings to provider format 145 | if ( version_compare( $current_db_version, '1.6.0', '<' ) ) { 146 | // get other options 147 | $other = $pvc->options['other']; 148 | 149 | // check if migration is needed 150 | if ( ! isset( $other['import_provider_settings'] ) && isset( $other['import_meta_key'] ) ) { 151 | $old_meta_key = $other['import_meta_key']; 152 | 153 | // create new provider settings structure 154 | $other['import_provider_settings'] = [ 155 | 'provider' => 'custom_meta_key', 156 | 'strategy' => 'merge', 157 | 'custom_meta_key' => [ 158 | 'meta_key' => $old_meta_key 159 | ] 160 | ]; 161 | 162 | // update settings 163 | update_option( 'post_views_counter_settings_other', $other ); 164 | 165 | // update options 166 | $pvc->options['other'] = $other; 167 | } 168 | } 169 | 170 | // move menu position setting to display tab 171 | $this->migrate_menu_position_option(); 172 | 173 | if ( isset( $_POST['post_view_counter_update'], $_POST['post_view_counter_number'] ) ) { 174 | if ( $_POST['post_view_counter_number'] === 'update_1' ) { 175 | $this->update_1(); 176 | 177 | // update plugin version 178 | update_option( 'post_views_counter_version', $pvc->defaults['version'], false ); 179 | } 180 | } 181 | 182 | // get current database version 183 | $current_db_version = get_option( 'post_views_counter_version', '1.0.0' ); 184 | 185 | // new version? 186 | if ( version_compare( $current_db_version, $pvc->defaults['version'], '<' ) ) { 187 | // is update 1 required? 188 | if ( version_compare( $current_db_version, '1.2.4', '<=' ) ) { 189 | $update_1_html = ' 190 |
191 | 192 |

' . __( 'Post Views Counter - this version requires a database update. Make sure to back up your database first.', 'post-views-counter' ) . '

193 |

194 |
'; 195 | 196 | $pvc->add_notice( $update_1_html, 'notice notice-info', false ); 197 | } else 198 | // update plugin version 199 | update_option( 'post_views_counter_version', $pvc->defaults['version'], false ); 200 | } 201 | } 202 | 203 | /** 204 | * Database update for 1.2.4 and below. 205 | * 206 | * @global object $wpdb 207 | * 208 | * @return void 209 | */ 210 | public function update_1() { 211 | global $wpdb; 212 | 213 | // get index 214 | $old_index = $wpdb->query( "SHOW INDEX FROM `" . $wpdb->prefix . "post_views` WHERE Key_name = 'id_period'" ); 215 | 216 | // check whether index already exists 217 | if ( $old_index > 0 ) { 218 | // drop unwanted index which prevented saving views with identical weeks and months 219 | $wpdb->query( "ALTER TABLE `" . $wpdb->prefix . "post_views` DROP INDEX id_period" ); 220 | } 221 | 222 | // get index 223 | $new_index = $wpdb->query( "SHOW INDEX FROM `" . $wpdb->prefix . "post_views` WHERE Key_name = 'id_type_period_count'" ); 224 | 225 | // check whether index already exists 226 | if ( $new_index === 0 ) { 227 | // create new index for better performance of sql queries 228 | $wpdb->query( 'ALTER TABLE `' . $wpdb->prefix . 'post_views` ADD UNIQUE INDEX `id_type_period_count` (`id`, `type`, `period`, `count`) USING BTREE' ); 229 | } 230 | 231 | Post_Views_Counter()->add_notice( __( 'Thank you! Datebase was successfully updated.', 'post-views-counter' ), 'updated', true ); 232 | } 233 | 234 | /** 235 | * Move menu position setting from "Other" to "Display" settings. 236 | * 237 | * @return void 238 | */ 239 | private function migrate_menu_position_option() { 240 | $pvc = Post_Views_Counter(); 241 | 242 | // prefer legacy value if present, otherwise fall back to current display option 243 | $menu_position = isset( $pvc->options['other']['menu_position'] ) && in_array( $pvc->options['other']['menu_position'], [ 'top', 'sub' ], true ) 244 | ? $pvc->options['other']['menu_position'] 245 | : ( isset( $pvc->options['display']['menu_position'] ) && in_array( $pvc->options['display']['menu_position'], [ 'top', 'sub' ], true ) 246 | ? $pvc->options['display']['menu_position'] 247 | : 'top' ); 248 | 249 | $display_options = get_option( 'post_views_counter_settings_display', [] ); 250 | 251 | if ( ! isset( $display_options['menu_position'] ) || ! in_array( $display_options['menu_position'], [ 'top', 'sub' ], true ) ) { 252 | $display_options['menu_position'] = $menu_position; 253 | update_option( 'post_views_counter_settings_display', $display_options ); 254 | } 255 | 256 | $other_options = get_option( 'post_views_counter_settings_other', [] ); 257 | 258 | if ( ! is_array( $other_options ) ) 259 | $other_options = []; 260 | 261 | if ( ! isset( $other_options['menu_position'] ) || ! in_array( $other_options['menu_position'], [ 'top', 'sub' ], true ) ) { 262 | $other_options['menu_position'] = $display_options['menu_position']; 263 | update_option( 'post_views_counter_settings_other', $other_options ); 264 | } 265 | 266 | $pvc->options['other']['menu_position'] = $other_options['menu_position']; 267 | $pvc->options['display']['menu_position'] = $display_options['menu_position']; 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /js/admin-dashboard.js: -------------------------------------------------------------------------------- 1 | ( function ( $ ) { 2 | 3 | /** 4 | * Load initial data. 5 | */ 6 | window.addEventListener( 'load', function () { 7 | pvcUpdatePostViewsWidget(); 8 | pvcUpdatePostMostViewedWidget(); 9 | } ); 10 | 11 | /** 12 | * Ready event. 13 | */ 14 | $( function () { 15 | // toggle collapse items 16 | $( '.pvc-accordion-header' ).on( 'click', function ( e ) { 17 | $( this ).closest( '.pvc-accordion-item' ).toggleClass( 'pvc-collapsed' ); 18 | 19 | var items = $( '#pvc-dashboard-accordion' ).find( '.pvc-accordion-item' ); 20 | var menuItems = {}; 21 | 22 | if ( items.length > 0 ) { 23 | $( items ).each( function ( index, item ) { 24 | var itemName = $( item ).attr( 'id' ); 25 | 26 | itemName = itemName.replace( 'pvc-', '' ); 27 | 28 | menuItems[itemName] = $( item ).hasClass( 'pvc-collapsed' ); 29 | } ); 30 | } 31 | 32 | // update user options 33 | pvcUpdateUserOptions( {menu_items: menuItems} ); 34 | } ); 35 | } ); 36 | 37 | // jQuery on an empty object, we are going to use this as our Queue 38 | var pvcAjaxQueue = $( {} ); 39 | 40 | /** 41 | * Put AJAX requests in a queue, run one request at a time and prevent overwriting user options. 42 | * 43 | * @param {type} ajaxOpts 44 | */ 45 | $.pvcAjaxQueue = function ( ajaxOpts ) { 46 | var jqXHR, 47 | dfd = $.Deferred(), 48 | promise = dfd.promise(); 49 | 50 | // run the actual query 51 | function doRequest( next ) { 52 | jqXHR = $.ajax( ajaxOpts ); 53 | jqXHR.done( dfd.resolve ) 54 | .fail( dfd.reject ) 55 | .then( next, next ); 56 | } 57 | 58 | // queue our ajax request 59 | pvcAjaxQueue.queue( doRequest ); 60 | 61 | // add the abort method 62 | promise.abort = function ( statusText ) { 63 | 64 | // proxy abort to the jqXHR if it is active 65 | if ( jqXHR ) { 66 | return jqXHR.abort( statusText ); 67 | } 68 | 69 | // if there wasn't already a jqXHR we need to remove from queue 70 | var queue = pvcAjaxQueue.queue(), 71 | index = $.inArray( doRequest, queue ); 72 | 73 | if ( index > - 1 ) { 74 | queue.splice( index, 1 ); 75 | } 76 | 77 | // and then reject the deferred 78 | dfd.rejectWith( ajaxOpts.context || ajaxOpts, [promise, statusText, ""] ); 79 | return promise; 80 | }; 81 | 82 | return promise; 83 | }; 84 | 85 | /** 86 | * Update user options. 87 | */ 88 | pvcUpdateUserOptions = function ( options ) { 89 | $.pvcAjaxQueue( { 90 | url: pvcArgs.ajaxURL, 91 | type: 'POST', 92 | dataType: 'json', 93 | data: { 94 | action: 'pvc_dashboard_user_options', 95 | nonce: pvcArgs.nonceUser, 96 | options: options 97 | }, 98 | success: function () {} 99 | } ); 100 | } 101 | 102 | /** 103 | * Update configuration. 104 | */ 105 | pvcUpdateConfig = function ( config, args ) { 106 | // update datasets 107 | config.data = args.data; 108 | 109 | // update tooltips with new dates 110 | config.options.plugins.tooltip = { 111 | callbacks: { 112 | title: function ( tooltip ) { 113 | return args.data.dates[tooltip[0].dataIndex]; 114 | } 115 | } 116 | }; 117 | 118 | // update colors 119 | $.each( config.data.datasets, function ( i, dataset ) { 120 | dataset.fill = args.design.fill; 121 | dataset.tension = 0.4; 122 | dataset.borderColor = args.design.borderColor; 123 | dataset.backgroundColor = args.design.backgroundColor; 124 | dataset.borderWidth = args.design.borderWidth; 125 | dataset.borderDash = args.design.borderDash; 126 | dataset.pointBorderColor = args.design.pointBorderColor; 127 | dataset.pointBackgroundColor = args.design.pointBackgroundColor; 128 | dataset.pointBorderWidth = args.design.pointBorderWidth; 129 | } ); 130 | 131 | return config; 132 | } 133 | 134 | /** 135 | * Get post most viewed data. 136 | */ 137 | function pvcGetPostMostViewedData( init, period, container ) { 138 | $( container ).addClass( 'loading' ).find( '.spinner' ).addClass( 'is-active' ); 139 | 140 | $.ajax( { 141 | url: pvcArgs.ajaxURL, 142 | type: 'POST', 143 | dataType: 'json', 144 | data: { 145 | action: 'pvc_dashboard_post_most_viewed', 146 | nonce: pvcArgs.nonce, 147 | period: period 148 | }, 149 | success: function ( response ) { 150 | // remove loader 151 | $( container ).removeClass( 'loading' ); 152 | $( container ).find( '.spinner' ).removeClass( 'is-active' ); 153 | 154 | // next call? 155 | if ( ! init ) 156 | pvcBindDateEvents( response.dates, container ); 157 | 158 | $( container ).find( '#pvc-post-most-viewed-content' ).html( response.html ); 159 | 160 | // trigger js event 161 | pvcTriggerEvent( 'pvc-dashboard-widget-loaded', response ); 162 | } 163 | } ); 164 | } 165 | 166 | /** 167 | * Get post views data. 168 | */ 169 | function pvcGetPostViewsData( init, period, container ) { 170 | $( container ).addClass( 'loading' ).find( '.spinner' ).addClass( 'is-active' ); 171 | 172 | $.ajax( { 173 | url: pvcArgs.ajaxURL, 174 | type: 'POST', 175 | dataType: 'json', 176 | data: { 177 | action: 'pvc_dashboard_post_views_chart', 178 | nonce: pvcArgs.nonce, 179 | period: period, 180 | lang: pvcArgs.lang ? pvcArgs.lang : '' 181 | }, 182 | success: function ( response ) { 183 | // remove loader 184 | $( container ).removeClass( 'loading' ); 185 | $( container ).find( '.spinner' ).removeClass( 'is-active' ); 186 | 187 | // first call? 188 | if ( init ) { 189 | var config = { 190 | type: 'line', 191 | options: { 192 | maintainAspectRatio: false, 193 | responsive: true, 194 | plugins: { 195 | legend: { 196 | display: true, 197 | position: 'bottom', 198 | align: 'center', 199 | fullSize: true, 200 | onHover: function ( e ) { 201 | e.native.target.style.cursor = 'pointer'; 202 | }, 203 | onLeave: function ( e ) { 204 | e.native.target.style.cursor = 'default'; 205 | }, 206 | onClick: function ( e, element, legend ) { 207 | var index = element.datasetIndex; 208 | var ci = legend.chart; 209 | var meta = ci.getDatasetMeta( index ); 210 | 211 | // set new hidden value 212 | if ( ci.isDatasetVisible( index ) ) 213 | meta.hidden = true; 214 | else 215 | meta.hidden = false; 216 | 217 | // rerender the chart 218 | ci.update(); 219 | 220 | // update user options 221 | pvcUpdateUserOptions( { 222 | post_type: ci.data.datasets[index].post_type, 223 | hidden: meta.hidden 224 | } ); 225 | }, 226 | labels: { 227 | boxWidth: 8, 228 | boxHeight: 8, 229 | font: { 230 | size: 13, 231 | weight: 'normal', 232 | family: "'-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Oxygen-Sans', 'Ubuntu', 'Cantarell', 'Helvetica Neue', 'sans-serif'" 233 | }, 234 | padding: 10, 235 | usePointStyle: false, 236 | textAlign: 'center' 237 | } 238 | } 239 | }, 240 | scales: { 241 | x: { 242 | display: true, 243 | title: { 244 | display: false 245 | } 246 | }, 247 | y: { 248 | display: true, 249 | grace: 0, 250 | beginAtZero: true, 251 | title: { 252 | display: false 253 | }, 254 | ticks: { 255 | precision: 0, 256 | maxTicksLimit: 12 257 | } 258 | } 259 | }, 260 | hover: { 261 | mode: 'label' 262 | } 263 | } 264 | }; 265 | 266 | config = pvcUpdateConfig( config, response ); 267 | 268 | window.postViewsChart = new Chart( document.getElementById( 'pvc-post-views-chart' ).getContext( '2d' ), config ); 269 | } else { 270 | pvcBindDateEvents( response.dates, container ); 271 | 272 | window.postViewsChart.config = pvcUpdateConfig( window.postViewsChart.config, response ); 273 | window.postViewsChart.update(); 274 | } 275 | 276 | // trigger js event 277 | pvcTriggerEvent( 'pvc-dashboard-widget-loaded', response ); 278 | } 279 | } ); 280 | } 281 | 282 | /** 283 | * Update post views widget. 284 | */ 285 | function pvcUpdatePostViewsWidget( period = '' ) { 286 | var container = $( '#pvc-post-views' ).find( '.pvc-dashboard-container' ); 287 | 288 | if ( $( container ).length > 0 ) { 289 | pvcBindDateEvents( false, container ); 290 | 291 | pvcGetPostViewsData( true, period, container ); 292 | } 293 | } 294 | 295 | /** 296 | * Update post most viewed widget. 297 | */ 298 | function pvcUpdatePostMostViewedWidget( period = '' ) { 299 | var container = $( '#pvc-post-most-viewed' ).find( '.pvc-dashboard-container' ); 300 | 301 | if ( $( container ).length > 0 ) { 302 | pvcBindDateEvents( false, container ); 303 | 304 | pvcGetPostMostViewedData( true, period, container ); 305 | } 306 | } 307 | 308 | /** 309 | * Bind date events. 310 | */ 311 | function pvcBindDateEvents( newDates, container ) { 312 | var dates = $( container ).find( '.pvc-date-nav' ); 313 | 314 | // replace dates? 315 | if ( newDates !== false ) 316 | dates[0].innerHTML = newDates; 317 | 318 | var prev = dates[0].getElementsByClassName( 'prev' )[0]; 319 | var next = dates[0].getElementsByClassName( 'next' )[0]; 320 | var id = $( container ).closest( '.pvc-accordion-item' ).attr( 'id' ); 321 | 322 | if ( id === 'pvc-post-most-viewed' ) 323 | prev.addEventListener( 'click', function ( e ) { 324 | e.preventDefault(); 325 | 326 | pvcLoadPostMostViewedData( e.target.dataset.date ); 327 | } ); 328 | else if ( id === 'pvc-post-views' ) 329 | prev.addEventListener( 'click', function ( e ) { 330 | e.preventDefault(); 331 | 332 | pvcLoadPostViewsData( e.target.dataset.date ); 333 | } ); 334 | 335 | // skip span 336 | if ( next.tagName === 'A' ) { 337 | if ( id === 'pvc-post-most-viewed' ) 338 | next.addEventListener( 'click', function ( e ) { 339 | e.preventDefault(); 340 | 341 | pvcLoadPostMostViewedData( e.target.dataset.date ); 342 | } ); 343 | else if ( id === 'pvc-post-views' ) 344 | next.addEventListener( 'click', function ( e ) { 345 | e.preventDefault(); 346 | 347 | pvcLoadPostViewsData( e.target.dataset.date ); 348 | } ); 349 | } 350 | } 351 | 352 | /** 353 | * Load post views data. 354 | */ 355 | function pvcLoadPostViewsData( period = '' ) { 356 | var container = $( '#pvc-post-views' ).find( '.pvc-dashboard-container' ); 357 | 358 | pvcGetPostViewsData( false, period, container ); 359 | } 360 | 361 | /** 362 | * Load post most viewed data. 363 | */ 364 | function pvcLoadPostMostViewedData( period = '' ) { 365 | var container = $( '#pvc-post-most-viewed' ).find( '.pvc-dashboard-container' ); 366 | 367 | pvcGetPostMostViewedData( false, period, container ); 368 | } 369 | 370 | /** 371 | * Trigger load widget JS event. 372 | */ 373 | function pvcTriggerEvent( name, response ) { 374 | // remove unneeded data 375 | const remove = ['dates', 'html', 'design'] 376 | 377 | remove.forEach( function ( prop ) { 378 | delete response[prop]; 379 | } ); 380 | 381 | // trigger event 382 | const event = new CustomEvent( name, { 383 | detail: response 384 | } ); 385 | 386 | window.dispatchEvent( event ); 387 | } 388 | 389 | } )( jQuery ); -------------------------------------------------------------------------------- /includes/class-widgets.php: -------------------------------------------------------------------------------- 1 | __( 'Displays a list of the most viewed posts', 'post-views-counter' ) 58 | ] 59 | ); 60 | 61 | // default settings 62 | $this->pvc_defaults = [ 63 | 'title' => __( 'Most Viewed Posts', 'post-views-counter' ), 64 | 'number_of_posts' => 5, 65 | 'period' => 'total', 66 | 'thumbnail_size' => 'thumbnail', 67 | 'post_type' => [], 68 | 'order' => 'desc', 69 | 'list_type' => 'unordered', 70 | 'show_post_views' => true, 71 | 'show_post_thumbnail' => false, 72 | 'show_post_excerpt' => false, 73 | 'show_post_author' => false, 74 | 'no_posts_message' => __( 'No most viewed posts found', 'post-views-counter' ) 75 | ]; 76 | 77 | // order types 78 | $this->pvc_order_types = [ 79 | 'asc' => __( 'Ascending', 'post-views-counter' ), 80 | 'desc' => __( 'Descending', 'post-views-counter' ) 81 | ]; 82 | 83 | // periods 84 | $this->pvc_periods = [ 85 | 'total' => __( 'Total Views', 'post-views-counter' ) 86 | ]; 87 | 88 | // list types 89 | $this->pvc_list_types = [ 90 | 'unordered' => __( 'Unordered list', 'post-views-counter' ), 91 | 'ordered' => __( 'Ordered list', 'post-views-counter' ) 92 | ]; 93 | 94 | // image sizes 95 | $this->pvc_image_sizes = array_merge( [ 'full' ], get_intermediate_image_sizes() ); 96 | 97 | // sort image sizes by name, ascending 98 | sort( $this->pvc_image_sizes, SORT_STRING ); 99 | } 100 | 101 | /** 102 | * Display widget. 103 | * 104 | * @param array $args 105 | * @param array $instance 106 | * @return void 107 | */ 108 | public function widget( $args, $instance ) { 109 | // empty title? 110 | if ( empty( $instance['title'] ) ) 111 | $instance['title'] = $this->pvc_defaults['title']; 112 | 113 | // filter title 114 | $instance['title'] = apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base ); 115 | 116 | $html = $args['before_widget'] . ( ! empty( $instance['title'] ) ? $args['before_title'] . esc_html( $instance['title'] ) . $args['after_title'] : '' ); 117 | $html .= pvc_most_viewed_posts( $instance, false ); 118 | $html .= $args['after_widget']; 119 | 120 | echo $html; 121 | } 122 | 123 | /** Render widget form. 124 | * 125 | * @param array $instance 126 | * @return void 127 | */ 128 | public function form( $instance ) { 129 | $html = ' 130 |

131 | 132 | 133 |

134 |

135 |
'; 136 | 137 | // post types 138 | foreach ( Post_Views_Counter()->functions->get_post_types() as $post_type => $post_type_name ) { 139 | $html .= ' 140 | '; 141 | } 142 | 143 | $html .= ' 144 |

145 |

146 | 147 | 159 |

160 |

161 | 162 | 163 |

164 |

165 | 166 | 167 |

168 |

169 | 170 | 180 |

181 |

182 | 183 | 193 |

194 |

195 | pvc_defaults['show_post_views'] ), false ) . ' /> 196 |
197 | pvc_defaults['show_post_excerpt'] ), false ) . ' /> 198 |
199 | pvc_defaults['show_post_author'] ), false ) . ' /> 200 |
201 | 202 |

203 | '; 218 | 219 | echo $html; 220 | } 221 | 222 | /** 223 | * Save widget form. 224 | * 225 | * @param array $new_instance 226 | * @param array $old_instance 227 | * @return array 228 | */ 229 | public function update( $new_instance, $old_instance ) { 230 | // number of posts 231 | $old_instance['number_of_posts'] = (int) (isset( $new_instance['number_of_posts'] ) ? $new_instance['number_of_posts'] : $this->pvc_defaults['number_of_posts']); 232 | 233 | // order 234 | $old_instance['order'] = isset( $new_instance['order'] ) && in_array( $new_instance['order'], array_keys( $this->pvc_order_types ), true ) ? $new_instance['order'] : $this->pvc_defaults['order']; 235 | 236 | // period 237 | $old_instance['period'] = isset( $new_instance['period'] ) && in_array( $new_instance['period'], array_keys( $this->pvc_periods ), true ) ? $new_instance['period'] : $this->pvc_defaults['period']; 238 | 239 | // list type 240 | $old_instance['list_type'] = isset( $new_instance['list_type'] ) && in_array( $new_instance['list_type'], array_keys( $this->pvc_list_types ), true ) ? $new_instance['list_type'] : $this->pvc_defaults['list_type']; 241 | 242 | // thumbnail size 243 | $old_instance['thumbnail_size'] = isset( $new_instance['thumbnail_size'] ) && in_array( $new_instance['thumbnail_size'], $this->pvc_image_sizes, true ) ? $new_instance['thumbnail_size'] : $this->pvc_defaults['thumbnail_size']; 244 | 245 | // booleans 246 | $old_instance['show_post_views'] = ! empty( $new_instance['show_post_views'] ); 247 | $old_instance['show_post_thumbnail'] = ! empty( $new_instance['show_post_thumbnail'] ); 248 | $old_instance['show_post_excerpt'] = ! empty( $new_instance['show_post_excerpt'] ); 249 | $old_instance['show_post_author'] = ! empty( $new_instance['show_post_author'] ); 250 | 251 | // texts 252 | $old_instance['title'] = sanitize_text_field( isset( $new_instance['title'] ) ? $new_instance['title'] : $this->pvc_defaults['title'] ); 253 | $old_instance['no_posts_message'] = sanitize_text_field( isset( $new_instance['no_posts_message'] ) ? $new_instance['no_posts_message'] : $this->pvc_defaults['no_posts_message'] ); 254 | 255 | // post types 256 | if ( isset( $new_instance['post_type'] ) ) { 257 | $post_types = []; 258 | 259 | // get post types 260 | $_post_types = Post_Views_Counter()->functions->get_post_types(); 261 | 262 | foreach ( $new_instance['post_type'] as $post_type ) { 263 | if ( isset( $_post_types[$post_type] ) ) 264 | $post_types[] = $post_type; 265 | } 266 | 267 | $old_instance['post_type'] = array_unique( $post_types ); 268 | } else 269 | $old_instance['post_type'] = [ 'post' ]; 270 | 271 | return $old_instance; 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Post Views Counter === 2 | Contributors: dfactory 3 | Tags: counter, postviews, statistics, analytics, pageviews 4 | Requires at least: 6.3.0 5 | Requires PHP: 7.4.0 6 | Tested up to: 6.9 7 | Stable tag: 1.6.0 8 | License: MIT License 9 | License URI: http://opensource.org/licenses/MIT 10 | 11 | Post Views Counter allows you to collect and display how many times a post, page, or other content has been viewed in a simple, fast and reliable way. 12 | 13 | == Description == 14 | 15 | Post Views Counter allows you to collect and display how many times a post, page, or other content has been viewed in a simple, fast and reliable way. 16 | 17 | [Post Views Counter](https://postviewscounter.com/) gives you clear, accurate post view stats — right inside WordPress. No external tools. No bloat. Just the numbers you need to see what’s working. 18 | 19 | = Key Benefits = 20 | 21 | Clarity, speed, and control: 22 | 23 | - **Clear, Focused Metrics** — You get a clear picture of how your posts are performing. 24 | - **Made for WordPress** — Runs entirely in your site. No GA, no third-party pipes; accurate counts in your Dashboard. 25 | - **Privacy-first** — Data lives on your server, with controls that respect visitors’ rights and privacy regulations. 26 | - **Works at scale** — Minimal overhead, no external scripts, Multisite-ready. 27 | - **Display anywhere** — Automatically show counts, or place them exactly where you want via blocks or shortcode. 28 | 29 | = Features = 30 | 31 | Practical features that matter: 32 | 33 | - Count & display views for **any post type** you select. 34 | - Three counting modes: **PHP, JavaScript, REST API** 35 | - Dashboard post views **stats widget** 36 | - Sortable Post Views **admin column** 37 | - Exclude bots, logged-in users, specific roles, or IPs 38 | - Manually adjust a post’s views when needed. 39 | - Query and **order content by views** (developer-friendly) 40 | - Custom REST API endpoints 41 | - Option to set count interval 42 | - One-click data import from **WP-PostViews**, **Statify** and **Page Views Count** 43 | - Post views **display position**, automatic or manual via shortcode 44 | - **Multisite** compatibile 45 | - **WPML/Polylang** compatible; translation-ready (.pot) 46 | 47 | = Post Views Counter Pro = 48 | 49 | More capability without extra complexity: 50 | 51 | - **Fast AJAX counting** for more accurate data. 52 | - **Caching optimization** that guarantees performance even under heavy traffic. 53 | - **Reports**: Views by Date, Post, Author to spot winners, trends, and top contributors. 54 | - Customizable **Views Period** (e.g., last 7/30 days) to control the views count timeframe. 55 | - **Export to CSV/XML** to download and share data. 56 | - **Integrations** for ordering by views in popular builders (e.g., **Elementor Pro, Divi, GenerateBlocks**). 57 | 58 | [Learn more about Pro →](https://postviewscounter.com/pricing/) 59 | 60 | == Installation == 61 | 62 | 1. Install Post Views Counter either via the WordPress.org plugin directory, or by uploading the files to your server 63 | 2. Activate the plugin through the 'Plugins' menu in WordPress 64 | 3. Go to the Post Views Counter settings and set your options. 65 | 66 | For many frequently asked questions check the [Post Views Counter Docs](https://postviewscounter.com/documentation/). 67 | 68 | = Why use Post Views Counter vs Google Analytics? = 69 | 70 | Post Views Counter gives you clean, per-post view counts inside WordPress — fast, cache-friendly, and privacy-first, with data that stays on your server. Google Analytics might be an overkill when you just need accurate post/page views for editorial decisions. 71 | 72 | = Can I use Post Views Counter alongside Google Analytics? = 73 | 74 | Of course — many sites use both. Post Views Counter handles on-site, per-post view counts inside WordPress (no third-party scripts), while Google Analytics covers marketing funnels and acquisition. 75 | 76 | = Is Post Views Counter GDPR compliant? = 77 | 78 | Post Views Counter runs entirely inside WordPress with no third-party scripts and keeps data on your server — aligning with GDPR-style expectations. 79 | 80 | = How do I get support? = 81 | 82 | If you’re using the free version, please post your question in the WordPress.org support forum. 83 | 84 | If you’ve purchased Post Views Counter Pro, your license includes one year of updates and premium support. You can contact us directly through our dedicated support channel available after logging into your account at [Post Views Counter](https://postviewscounter.com/), and our team will get back to you. 85 | 86 | == Screenshots == 87 | 88 | 1. screenshot-1.png 89 | 2. screenshot-2.png 90 | 91 | == Changelog == 92 | 93 | = 1.6.0 = 94 | * New: Dedicated import framework with provider-aware analysis/reporting and strategy selector. 95 | * New: Option to import views from Statify and Page Views Count plugins. 96 | * New (Pro): Additional import strategies (skip existing, keep higher count, fill empty-only). 97 | * New: Plugin Status panel now surfaces detected PVC database tables for easier troubleshooting. 98 | * Tweak: Settings UI reorganized with refreshed copy, clearer visitor exclusion controls, and a polished Other tab experience. 99 | * Tweak: Menu placement option moved to Display settings and mirrored for backward compatibility. 100 | 101 | = 1.5.9 = 102 | * New: Admin column modal chart with post views data 103 | * New: Extended admin column modal with yearly and weekly views data (Pro) 104 | * New: Admin column modal chart for terms and users (Pro) 105 | 106 | = 1.5.8 = 107 | * Tweak: Updated default value for object cache flushing interval 108 | * Tweak: Treat empty or missing user agent as bot 109 | 110 | = 1.5.7 = 111 | * New: Count visits by referrer (Pro) 112 | * Prevent duplicate AJAX calls in REST API mode 113 | * Fix: Major improvements for FastAjax handling (Pro) 114 | * Fix: Major object cache support improvements (Pro) 115 | * Fix: Apply crawler/bot check filter for REST API endpoints 116 | * Tweak: Remove unused storage and mutator methods 117 | 118 | = 1.5.6 = 119 | * New: Count visits by device, browser and OS (Pro) 120 | * New: Count visits by browser language (Pro) 121 | * New: Traffic Information dashboard widget (Pro) 122 | * New: HTTP request improvements for caching and security (Pro) 123 | * New: Client size bot detection (Pro) 124 | * Tweak: Fix and simplify post views shortcode for loops 125 | * Tweak: Adjust the post views display in Gutenberg editor 126 | * Tweak: Check db query results and log error 127 | 128 | = 1.5.5 = 129 | * New: Count Time option to store the views in GMT or Local time (Pro) 130 | * New: Reports extended with Author Posts and Author Archive (Pro) 131 | * New: Counting Jet Engine Profile Builder user profiles as archive view (Pro) 132 | * Tweak: Improved logic for Admin Display and Admin Edit 133 | * Tweak: Settings UI improvements 134 | 135 | = 1.5.4 = 136 | * New: Caching compatibility option (Pro) 137 | 138 | = 1.5.3 = 139 | * Tweak: WordPress 6.8 compatibility 140 | * Tweak: Move admin column options to Display settings 141 | * Tweak: Added pvc_current_scheme_color filter hook to adjust chart colors 142 | 143 | = 1.5.2 = 144 | * Tweak: Updated crawlers list 145 | * Tweak: Updated Chart.js to 4.4.8 146 | * New: Add orderby post_views support to Elementor Pro posts query (Pro) 147 | * New: Add orderby post_views support to Divi theme blog module (Pro) 148 | * New: Add orderby post_views support to GenerateBlocks query (Pro) 149 | * New: Option to exclude AI bots visits from counting (Pro) 150 | 151 | = 1.5.1 = 152 | * Fix: Undefined variable $post_type warning in admin columns 153 | 154 | = 1.5.0 = 155 | * Fix: Deprecated DateTime dynamic property 156 | * Tweak:Implement AJAX queue for saving dashboard user options 157 | * Tweak: Update bot detection class 158 | * Tweak: Add widget loaded JS event 159 | * Tweak: Fix typo in widget tooltip 160 | * Tweak: Improve dahboard widgets UI 161 | * New: Dashboard widgets revamp (Pro) 162 | * New: Added weekly and yearly dashboard widgets navigation (Pro) 163 | * New: Added trend (increase/decrease) to dashboard widget charts (Pro) 164 | * New: Taxonomy & Terms selection in Views by Post reports (Pro) 165 | 166 | = 1.4.8 = 167 | * New: Introducing Post Views block 168 | * New: Introducing Most Viewed Posts block 169 | * Tweak: Updated Chart.js to 4.4.6 170 | 171 | = 1.4.7 = 172 | * New: Dynamic views loading option (Pro) 173 | * Fix: Multi-sorting queries with post_views orderby parameter 174 | 175 | = 1.4.6 = 176 | * Fix: Bulk posts selection 177 | * Fix: Additional SQL queries escaping 178 | * Tweak: Call to undefined function is_favicon() 179 | * Tweak: Enqueue main script in header instead of footer 180 | * Tweak: Better JS error handling 181 | * Tweak: Updated Chart.js to 4.4.2 182 | 183 | = 1.4.5 = 184 | * Fix: Post views bulk saving security 185 | * Tweak: Removed WP Rocket as bot in crawler detection 186 | 187 | = 1.4.4 = 188 | * New: Option to enter meta_key for importing the views 189 | * New: Revamped Reports for Views by Date, Views by Post and Views by Author (Pro) 190 | * New: REST API support for post, site, term and user views (Pro) 191 | * New: Views Period option to display views from a selected time period instead of total (Pro) 192 | * New: [site-views] shortcode for total site views display (Pro) 193 | * Tweak: Improved icon handling 194 | * Tweak: Updated crawler detection 195 | 196 | = 1.4.3 = 197 | * Tweak: Update languages file 198 | 199 | = 1.4.2 = 200 | * New: Option to select position of the plugin menu 201 | 202 | = 1.4.1 = 203 | * Fix: Frontpage views not recorded properly 204 | 205 | = 1.4 = 206 | * New: Introducing Post Views Counter Pro 207 | * New: Fast Ajax views counting mode (Pro) 208 | * New: Google AMP support (Pro) 209 | * New: Taxonomy term views (Pro) 210 | * New: Author archive views (Pro) 211 | * New: Cookies/Cookieless data storage option (Pro) 212 | * New: Dedicated Reports page (Pro) 213 | * New: Exporting views to CSV or XML files (Pro) 214 | * Tweak: Improved validation and sanitization 215 | * Tweak: Chart.js updated to 4.3.0 216 | 217 | = 1.3.13 = 218 | * New: Compatibility with WP 6.2 and PHP 8.2 219 | * Fix: Invalid year in seconds 220 | * Fix: Possible invalid cookie data in views storage 221 | * Fix: Default database prefix 222 | * Tweak: Switch from wp_localize_script to wp_add_inline_script 223 | * Tweak: Updated bot detection 224 | 225 | 226 | = 1.3.12 = 227 | * Fix: Frontend Javascript rewritten from jQuery to Vanilla JS 228 | * Fix: Admin Bar Style loading on every page 229 | * Fix: Network initialization process for new sites 230 | * Fix: IP address encryption 231 | * Fix: REST API endpoints 232 | * Fix: Removed couple of deprecated functions 233 | * Tweak: Updated chart.js script to version 3.9.1 234 | * Tweak: Added SameSite attribute to cookie 235 | 236 | = 1.3.11 = 237 | * Fix: Potentailly incorrect counting of post views in edge case db queries 238 | * Fix: Possible empty chart in dashboard 239 | * Fix: Incorrect saving of dashboard widget user options 240 | * Tweak: Updated Chart.js to version 3.7.0 241 | 242 | = 1.3.10 = 243 | * Fix: Post views column not working properly 244 | * Tweak: Switched to openssl_encrypt method for IP encryption 245 | * Tweak: Improved user input escaping 246 | 247 | = 1.3.9 = 248 | * Tweak: Remove unnecessary plugin files 249 | 250 | = 1.3.8 = 251 | * Tweak: Improved user input escaping 252 | 253 | = 1.3.7 = 254 | * Tweak: Implemented internal settings API 255 | 256 | = 1.3.6 = 257 | * Fix: Option to hide admin bar chart 258 | 259 | = 1.3.5 = 260 | * New: Option to hide admin bar chart 261 | * Fix: Small security bug with views label 262 | * Tweak: Remove unnecessary CSS on every page 263 | 264 | = 1.3.4 = 265 | * New: Post Views stats preview in the admin bar 266 | * New: Top Posts data available in the dashboard widget 267 | * Tweak: Improved privacy using IP encrypting 268 | * Tweak: PHP 8.x compatibility 269 | 270 | = 1.3.3 = 271 | * Fix: PHP Notice: Trying to get property 'colors' of non-object 272 | * Fix: PHP Notice: register_rest_route was called incorrectly 273 | 274 | = 1.3.2 = 275 | * New: Introducing dashboard widget navigation 276 | * New: Counter support for Media (attachments) 277 | * Tweak: Extended views query for handling complex date/time requests 278 | 279 | = 1.3.1 = 280 | * Fix: Gutenberg CSS file missing 281 | * Tweak: POT translation file update 282 | 283 | = 1.3 = 284 | * New: Gutenberg compatibility 285 | * New: Additional options in widgets: post author and display style 286 | * Fix: Undefined variables when IP saving enabled 287 | * Fix: Check cookie not being triggered in Fast Ajax mode 288 | * Fix: Invalid arguments in implode function causing warning 289 | * Fix: Thumbnail size option did not show up after thumbnail checkbox was checked 290 | * Fix: Saving post (in quick edit mode too) did not update post views 291 | 292 | = 1.2.14 = 293 | * Fix: Bulk edit post views count reset issue 294 | 295 | = 1.2.13 = 296 | * New: Experimental Fast AJAX counter method (10+ times faster) 297 | 298 | = 1.2.12 = 299 | * New: GDPR compatibility with Cookie Notice plugin 300 | 301 | = 1.2.11 = 302 | * Tweak: Additional IP expiration checks added as an option 303 | 304 | = 1.2.10 = 305 | * New: Additional transient based IP expiration checks 306 | * Tweak: Chart.js script update to 2.7.1 307 | 308 | = 1.2.9 = 309 | * Fix: WooCommerce products list table broken 310 | 311 | = 1.2.8 = 312 | * New: Multisite compatibility 313 | * Fix: Undefined index post_views_column on post_views_counter/includes/settings.php 314 | * Tweak: Improved user IP handling 315 | 316 | = 1.2.7 = 317 | * Fix: Chart data not updating for object cached installs due to missing expire parameter 318 | * Fix: Bug preventing hiding the counter based on user role. 319 | * Fix: Undefined notice in the admin dashboard request 320 | 321 | = 1.2.6 = 322 | * Fix: Hardcoded post_views database table prefix 323 | 324 | = 1.2.5 = 325 | * New: REST API counter mode 326 | * New: Adjust dashboard chart colors to admin color scheme 327 | * Tweak: Dashboard chart query optimization 328 | * Tweak: post_views database table optimization 329 | * Tweak: Added plugin documentation link 330 | 331 | = 1.2.4 = 332 | * New: Advanced crawler detection 333 | * Tweak: Chart.js script update to 2.4.0 334 | 335 | = 1.2.3 = 336 | * New: IP wildcard support 337 | * Tweak: Delete post_views database table on deactivation 338 | 339 | = 1.2.2 = 340 | * Fix: Notice undefined variable: post_ids, thanks to [zytzagoo](https://github.com/zytzagoo) 341 | * Tweak: Switched translation files storage, from local to WP repository 342 | 343 | = 1.2.1 = 344 | * New: Option to display post views on select page types 345 | * Tweak: Dashboard widget query optimization 346 | 347 | = 1.2.0 = 348 | * New: Dashboard post views stats widget 349 | * Fix: A couple of typos in method names 350 | 351 | = 1.1.4 = 352 | * Fix: Dashicons link broken. 353 | * Tweak: Confirmed WordPress 4.4 compatibility 354 | 355 | = 1.1.3 = 356 | * Fix: Duplicated views count in custom post types 357 | * Fix: Exclude visitors checkboxes not working 358 | 359 | = 1.1.2 = 360 | * Fix: Most viewed posts widget broken 361 | 362 | = 1.1.1 = 363 | * Tweak: Enable edit views on new post. 364 | * Tweak: Extend WP_Query post data with post_views 365 | 366 | = 1.1.0 = 367 | * New: Quick post views edit 368 | * New: Bulk post views edit 369 | * Tweak: Admin UI improvements 370 | 371 | = 1.0.12 = 372 | * New: Italian translation, thanks to [Rene Querin](http://www.q-design.it) 373 | 374 | = 1.0.11 = 375 | * New: French translation, thanks to [Theophil Bethel](http://reseau-chretien-gironde.fr/) 376 | 377 | = 1.0.10 = 378 | * New: Option to limit post views editing to admins only 379 | 380 | = 1.0.9 = 381 | * New: Spanish translation, thanks to [Carlos Rodriguez](http://cglevel.com/) 382 | 383 | = 1.0.8 = 384 | * New: Croation translation, thanks to [Tomas Trkulja](http://zytzagoo.net/blog/) 385 | 386 | = 1.0.7 = 387 | * New: Possibility to manually set views count for each post 388 | * New: Plugin development moved to [dFactory GitHub Repository](https://github.com/dfactoryplugins) 389 | 390 | = 1.0.6 = 391 | * New: Object cache support, thanks to [Tomas Trkulja](http://zytzagoo.net/blog/) 392 | * New: Hebrew translation, thanks to [Ahrale Shrem](http://atar4u.com/) 393 | 394 | = 1.0.5 = 395 | * Tweak: Added number_format_i18n for displayed views count 396 | * Tweak: Additional action hook for developers 397 | 398 | = 1.0.4 = 399 | * Fix: Possible issue with remove_post_views_count function 400 | 401 | = 1.0.3 = 402 | * New: Russian translation, thanks to moonkir 403 | * Fix: Remove [post-views] shortcode from post excerpts if excerpt is empty 404 | 405 | = 1.0.2 = 406 | * Fix: Pluggable functions initialized too lately 407 | 408 | = 1.0.0 = 409 | Initial release 410 | 411 | == Upgrade Notice == 412 | 413 | = 1.6.0 = 414 | New provider-based import framework (Statify/Page Views Count support, smarter strategies), refreshed settings UI -------------------------------------------------------------------------------- /includes/class-query.php: -------------------------------------------------------------------------------- 1 | query_vars['orderby'] ) ) 55 | return; 56 | 57 | if ( is_string( $query->query_vars['orderby'] ) ) { 58 | // simple order by post_views 59 | if ( $query->query_vars['orderby'] === 'post_views' ) 60 | $query->pvc_orderby = true; 61 | // multisort post_views as string 62 | elseif ( strpos( $query->query_vars['orderby'], 'post_views' ) !== false ) { 63 | // explode orderby 64 | $sort = explode( ' ', $query->query_vars['orderby'] ); 65 | 66 | // make sure only full string is available 67 | if ( ! empty( $sort ) ) { 68 | // clear it 69 | $sort = array_filter( $sort ); 70 | 71 | if ( in_array( 'post_views', $sort, true ) ) 72 | $query->pvc_orderby = true; 73 | } 74 | } 75 | // post_views in array 76 | } elseif ( is_array( $query->query_vars['orderby'] ) && array_key_exists( 'post_views', $query->query_vars['orderby'] ) ) 77 | $query->pvc_orderby = true; 78 | } 79 | 80 | /** 81 | * Modify the database query to use post_views parameter. 82 | * 83 | * @global object $wpdb 84 | * 85 | * @param string $join 86 | * @param object $query 87 | * @return string 88 | */ 89 | public function posts_join( $join, $query ) { 90 | $sql = ''; 91 | $query_chunks = []; 92 | 93 | // views query? 94 | if ( ! empty( $query->query['views_query'] ) ) { 95 | if ( isset( $query->query['views_query']['inclusive'] ) ) 96 | $query->query['views_query']['inclusive'] = (bool) $query->query['views_query']['inclusive']; 97 | else 98 | $query->query['views_query']['inclusive'] = true; 99 | 100 | // check after and before dates 101 | foreach ( [ 'after' => '>', 'before' => '<' ] as $date => $type ) { 102 | $year_ = null; 103 | $month_ = null; 104 | $week_ = null; 105 | $day_ = null; 106 | 107 | // check views query date 108 | if ( ! empty( $query->query['views_query'][$date] ) ) { 109 | // is it a date array? 110 | if ( is_array( $query->query['views_query'][$date] ) ) { 111 | // check views query $date date year 112 | if ( ! empty( $query->query['views_query'][$date]['year'] ) ) 113 | $year_ = str_pad( (int) $query->query['views_query'][$date]['year'], 4, 0, STR_PAD_LEFT ); 114 | 115 | // check views query date month 116 | if ( ! empty( $query->query['views_query'][$date]['month'] ) ) 117 | $month_ = str_pad( (int) $query->query['views_query'][$date]['month'], 2, 0, STR_PAD_LEFT ); 118 | 119 | // check views query date week 120 | if ( ! empty( $query->query['views_query'][$date]['week'] ) ) 121 | $week_ = str_pad( (int) $query->query['views_query'][$date]['week'], 2, 0, STR_PAD_LEFT ); 122 | 123 | // check views query date day 124 | if ( ! empty( $query->query['views_query'][$date]['day'] ) ) 125 | $day_ = str_pad( (int) $query->query['views_query'][$date]['day'], 2, 0, STR_PAD_LEFT ); 126 | // is it a date string? 127 | } elseif ( is_string( $query->query['views_query'][$date] ) ) { 128 | $time_ = strtotime( $query->query['views_query'][$date] ); 129 | 130 | // valid datetime? 131 | if ( $time_ !== false ) { 132 | // week does not exists here, string dates are always treated as year + month + day 133 | list( $day_, $month_, $year_ ) = explode( ' ', date( "d m Y", $time_ ) ); 134 | } 135 | } 136 | 137 | // valid date? 138 | if ( ! ( $year_ === null && $month_ === null && $week_ === null && $day_ === null ) ) { 139 | $query_chunks[] = [ 140 | 'year' => $year_, 141 | 'month' => $month_, 142 | 'day' => $day_, 143 | 'week' => $week_, 144 | 'type' => $type . ( $query->query['views_query']['inclusive'] ? '=' : '' ) 145 | ]; 146 | } 147 | } 148 | } 149 | 150 | // any after, before query chunks? 151 | if ( ! empty( $query_chunks ) ) { 152 | $valid_dates = true; 153 | 154 | // check only if both dates are in query 155 | if ( count( $query_chunks ) === 2 ) { 156 | // before and after dates should be the same 157 | foreach ( [ 'year', 'month', 'day', 'week' ] as $date_type ) { 158 | if ( ! ( ( $query_chunks[0][$date_type] !== null && $query_chunks[1][$date_type] !== null ) || ( $query_chunks[0][$date_type] === null && $query_chunks[1][$date_type] === null ) ) ) 159 | $valid_dates = false; 160 | } 161 | } 162 | 163 | // after and before dates should be both valid 164 | if ( $valid_dates ) { 165 | foreach ( $query_chunks as $chunk ) { 166 | // year 167 | if ( isset( $chunk['year'] ) ) { 168 | // year, week 169 | if ( isset( $chunk['week'] ) ) 170 | $sql .= " AND pvc.type = 1 AND pvc.period " . $chunk['type'] . " '" . $chunk['year'] . $chunk['week'] . "'"; 171 | // year, month 172 | elseif ( isset( $chunk['month'] ) ) { 173 | // year, month, day 174 | if ( isset( $chunk['day'] ) ) 175 | $sql .= " AND pvc.type = 0 AND pvc.period " . $chunk['type'] . " '" . $chunk['year'] . $chunk['month'] . $chunk['day'] . "'"; 176 | // year, month 177 | else 178 | $sql .= " AND pvc.type = 2 AND pvc.period " . $chunk['type'] . " '" . $chunk['year'] . $chunk['month'] . "'"; 179 | // year 180 | } else 181 | $sql .= " AND pvc.type = 3 AND pvc.period " . $chunk['type'] . " '" . $chunk['year'] . "'"; 182 | // month 183 | } elseif ( isset( $chunk['month'] ) ) { 184 | // month, day 185 | if ( isset( $chunk['day'] ) ) 186 | $sql .= " AND pvc.type = 0 AND RIGHT( pvc.period, 4 ) " . $chunk['type'] . " '" . $chunk['month'] . $chunk['day'] . "'"; 187 | // month 188 | else 189 | $sql .= " AND pvc.type = 2 AND RIGHT( pvc.period, 2 ) " . $chunk['type'] . " '" . $chunk['month'] . "'"; 190 | // week 191 | } elseif ( isset( $chunk['week'] ) ) 192 | $sql .= " AND pvc.type = 1 AND RIGHT( pvc.period, 2 ) " . $chunk['type'] . " '" . $chunk['week'] . "'"; 193 | // day 194 | elseif ( isset( $chunk['day'] ) ) 195 | $sql .= " AND pvc.type = 0 AND RIGHT( pvc.period, 2 ) " . $chunk['type'] . " '" . $chunk['day'] . "'"; 196 | } 197 | } 198 | } 199 | 200 | // standard query 201 | if ( $sql === '' ) { 202 | // check year 203 | if ( isset( $query->query['views_query']['year'] ) ) 204 | $year = (int) $query->query['views_query']['year']; 205 | 206 | // check month 207 | if ( isset( $query->query['views_query']['month'] ) ) 208 | $month = (int) $query->query['views_query']['month']; 209 | 210 | // check week 211 | if ( isset( $query->query['views_query']['week'] ) ) 212 | $week = (int) $query->query['views_query']['week']; 213 | 214 | // check day 215 | if ( isset( $query->query['views_query']['day'] ) ) 216 | $day = (int) $query->query['views_query']['day']; 217 | 218 | // year 219 | if ( isset( $year ) ) { 220 | // year, week 221 | if ( isset( $week ) && $this->is_date_valid( 'yw', $year, 0, 0, $week ) ) 222 | $sql = " AND pvc.type = 1 AND pvc.period = '" . str_pad( $year, 4, 0, STR_PAD_LEFT ) . str_pad( $week, 2, 0, STR_PAD_LEFT ) . "'"; 223 | // year, month 224 | elseif ( isset( $month ) ) { 225 | // year, month, day 226 | if ( isset( $day ) && $this->is_date_valid( 'ymd', $year, $month, $day ) ) 227 | $sql = " AND pvc.type = 0 AND pvc.period = '" . str_pad( $year, 4, 0, STR_PAD_LEFT ) . str_pad( $month, 2, 0, STR_PAD_LEFT ) . str_pad( $day, 2, 0, STR_PAD_LEFT ) . "'"; 228 | // year, month 229 | elseif ( $this->is_date_valid( 'ym', $year, $month ) ) 230 | $sql = " AND pvc.type = 2 AND pvc.period = '" . str_pad( $year, 4, 0, STR_PAD_LEFT ) . str_pad( $month, 2, 0, STR_PAD_LEFT ) . "'"; 231 | // year 232 | } elseif ( $this->is_date_valid( 'y', $year ) ) 233 | $sql = " AND pvc.type = 3 AND pvc.period = '" . str_pad( $year, 4, 0, STR_PAD_LEFT ) . "'"; 234 | // month 235 | } elseif ( isset( $month ) ) { 236 | // month, day 237 | if ( isset( $day ) && $this->is_date_valid( 'md', 0, $month, $day ) ) { 238 | $sql = " AND pvc.type = 0 AND RIGHT( pvc.period, 4 ) = '" . str_pad( $month, 2, 0, STR_PAD_LEFT ) . str_pad( $day, 2, 0, STR_PAD_LEFT ) . "'"; 239 | // month 240 | } elseif ( $this->is_date_valid( 'm', 0, $month ) ) 241 | $sql = " AND pvc.type = 2 AND RIGHT( pvc.period, 2 ) = '" . str_pad( $month, 2, 0, STR_PAD_LEFT ) . "'"; 242 | // week 243 | } elseif ( isset( $week ) && $this->is_date_valid( 'w', 0, 0, 0, $week ) ) 244 | $sql = " AND pvc.type = 1 AND RIGHT( pvc.period, 2 ) = '" . str_pad( $week, 2, 0, STR_PAD_LEFT ) . "'"; 245 | // day 246 | elseif ( isset( $day ) && $this->is_date_valid( 'd', 0, 0, $day ) ) 247 | $sql = " AND pvc.type = 0 AND RIGHT( pvc.period, 2 ) = '" . str_pad( $day, 2, 0, STR_PAD_LEFT ) . "'"; 248 | } 249 | 250 | if ( $sql !== '' ) 251 | $query->pvc_query = true; 252 | } 253 | 254 | // is it sorted by post views? 255 | if ( ( $sql === '' && isset( $query->pvc_orderby ) && $query->pvc_orderby ) || apply_filters( 'pvc_extend_post_object', false, $query ) === true ) 256 | $sql = ' AND pvc.type = 4'; 257 | 258 | // add date range 259 | if ( $sql !== '' ) { 260 | global $wpdb; 261 | 262 | $join .= " LEFT JOIN " . $wpdb->prefix . "post_views pvc ON pvc.id = " . $wpdb->prefix . "posts.ID" . $sql; 263 | 264 | $this->join_sql = $join; 265 | } 266 | 267 | return $join; 268 | } 269 | 270 | /** 271 | * Group posts using the post ID. 272 | * 273 | * @global object $wpdb 274 | * @global string $pagenow 275 | * 276 | * @param string $groupby 277 | * @param object $query 278 | * @return string 279 | */ 280 | public function posts_groupby( $groupby, $query ) { 281 | // is it sorted by post views or views_query is used? 282 | if ( ( isset( $query->pvc_orderby ) && $query->pvc_orderby ) || ( isset( $query->pvc_query ) && $query->pvc_query ) || apply_filters( 'pvc_extend_post_object', false, $query ) === true ) { 283 | global $pagenow; 284 | 285 | // needed only for sorting 286 | if ( $pagenow === 'upload.php' || $pagenow === 'edit.php' ) 287 | $query->query['views_query']['hide_empty'] = false; 288 | 289 | global $wpdb; 290 | 291 | $groupby = trim( $groupby ); 292 | $groupby_aliases = []; 293 | $groupby_values = []; 294 | $groupby_sql = ''; 295 | $groupby_set = false; 296 | 297 | // standard group by 298 | if ( strpos( $groupby, $wpdb->prefix . 'posts.ID' ) === false ) 299 | $groupby_aliases[] = $wpdb->prefix . 'posts.ID'; 300 | else 301 | $groupby_set = true; 302 | 303 | // tax query group by 304 | $groupby_aliases[] = $this->get_groupby_meta_aliases( $query ); 305 | 306 | // meta query group by 307 | if ( $this->join_sql ) { 308 | $groupby_aliases[] = $this->get_groupby_tax_aliases( $query, $this->join_sql ); 309 | 310 | // clear join to avoid possible issues 311 | $this->join_sql = ''; 312 | } 313 | 314 | // any group by aliases? 315 | if ( ! empty( $groupby_aliases ) ) { 316 | foreach ( $groupby_aliases as $alias ) { 317 | if ( is_array( $alias ) ) { 318 | $groupby_values = array_merge( $groupby_values, $alias ); 319 | } else 320 | $groupby_values[] = $alias; 321 | } 322 | } 323 | 324 | // any group by values? 325 | if ( ! empty( $groupby_values ) ) { 326 | $groupby = ( $groupby !== '' ? $groupby . ', ' : '' ) . implode( ', ', $groupby_values ); 327 | 328 | // set group by flag 329 | $groupby_set = true; 330 | } 331 | 332 | if ( $groupby_set ) 333 | $query->pvc_groupby = true; 334 | 335 | // hide empty? 336 | if ( ! isset( $query->query['views_query']['hide_empty'] ) || $query->query['views_query']['hide_empty'] === true ) 337 | $groupby .= ' HAVING post_views > 0'; 338 | } 339 | 340 | return $groupby; 341 | } 342 | 343 | /** 344 | * Order posts by post views. 345 | * 346 | * @global object $wpdb 347 | * 348 | * @param string $orderby 349 | * @param object $query 350 | * @return string 351 | */ 352 | public function posts_orderby( $orderby, $query ) { 353 | // is it sorted by post views? 354 | if ( ( isset( $query->pvc_orderby ) && $query->pvc_orderby ) ) { 355 | global $wpdb; 356 | 357 | // get order 358 | $order = $query->get( 'order' ); 359 | 360 | // get original orderby (before parsing) 361 | $org_orderby = $query->get( 'orderby' ); 362 | 363 | // orderby as string 364 | if ( is_string( $org_orderby ) ) { 365 | if ( $org_orderby === 'post_views' ) 366 | $orderby = 'post_views ' . $order; 367 | elseif ( strpos( $org_orderby, 'post_views' ) !== false ) { 368 | // explode orderby 369 | $sort = explode( ' ', $org_orderby ); 370 | 371 | if ( ! empty( $sort ) ) { 372 | // clear it 373 | $sort = array_values( array_filter( $sort ) ); 374 | 375 | // make sure only full string is available 376 | if ( in_array( 'post_views', $sort, true ) ) { 377 | // sort only by post views 378 | if ( count( $sort ) === 1 ) 379 | $orderby = 'post_views ' . $order; 380 | else { 381 | // post_views as first value 382 | if ( $sort[0] === 'post_views' ) 383 | $orderby = 'post_views ' . $order . ', ' . $orderby; 384 | else { 385 | //todo find a way to recognize other sorting options based on original order and parsed order by wordpress 386 | $orderby = 'post_views ' . $order . ', ' . $orderby; 387 | } 388 | } 389 | } 390 | } 391 | } 392 | // orderby as array 393 | } elseif ( is_array( $org_orderby ) && array_key_exists( 'post_views', $org_orderby ) ) { 394 | // sort only by post views 395 | if ( count( $org_orderby ) === 1 ) 396 | $orderby = 'post_views ' . $order; 397 | else { 398 | // post_views as first key 399 | if ( array_key_first( $org_orderby ) === 'post_views' ) { 400 | $sanitized_orderby = sanitize_sql_orderby( 'post_views ' . strtoupper( $org_orderby['post_views'] ) ); 401 | 402 | if ( $sanitized_orderby !== false ) 403 | $orderby = $sanitized_orderby . ', ' . $orderby; 404 | else 405 | $orderby = 'post_views ' . $order . ', ' . $orderby; 406 | } else { 407 | //todo find a way to recognize other sorting options based on original order and parsed order by wordpress 408 | $sanitized_orderby = sanitize_sql_orderby( 'post_views ' . strtoupper( $org_orderby['post_views'] ) ); 409 | 410 | if ( $sanitized_orderby !== false ) 411 | $orderby = $sanitized_orderby . ', ' . $orderby; 412 | else 413 | $orderby = 'post_views ' . $order . ', ' . $orderby; 414 | } 415 | } 416 | } 417 | } 418 | 419 | return $orderby; 420 | } 421 | 422 | /** 423 | * Add DISTINCT clause. 424 | * 425 | * @param string $distinct 426 | * @param object $query 427 | * @return string 428 | */ 429 | public function posts_distinct( $distinct, $query ) { 430 | if ( ( ( isset( $query->pvc_groupby ) && $query->pvc_groupby ) || ( isset( $query->pvc_orderby ) && $query->pvc_orderby ) || ( isset( $query->pvc_query ) && $query->pvc_query ) || apply_filters( 'pvc_extend_post_object', false, $query ) === true ) && ( strpos( $distinct, 'DISTINCT' ) === false ) ) 431 | $distinct = $distinct . ' DISTINCT '; 432 | 433 | return $distinct; 434 | } 435 | 436 | /** 437 | * Return post views in queried post objects. 438 | * 439 | * @param string $fields 440 | * @param object $query 441 | * @return string 442 | */ 443 | public function posts_fields( $fields, $query ) { 444 | if ( ( ! isset( $query->query['fields'] ) || $query->query['fields'] === '' || $query->query['fields'] === 'all' ) && ( ( isset( $query->pvc_orderby ) && $query->pvc_orderby ) || ( isset( $query->pvc_query ) && $query->pvc_query ) || apply_filters( 'pvc_extend_post_object', false, $query ) === true ) ) 445 | $fields = $fields . ', SUM( COALESCE( pvc.count, 0 ) ) AS post_views'; 446 | 447 | return $fields; 448 | } 449 | 450 | /** 451 | * Get tax table aliases from query. 452 | * 453 | * @global object $wpdb 454 | * 455 | * @param object $query 456 | * @param string $join_sql 457 | * @return array 458 | */ 459 | private function get_groupby_tax_aliases( $query, $join_sql ) { 460 | global $wpdb; 461 | 462 | $groupby = []; 463 | 464 | // trim join sql 465 | $join_sql = trim( $join_sql ); 466 | 467 | // any join sql? valid query with tax query? 468 | if ( $join_sql !== '' && is_a( $query, 'WP_Query' ) && ! empty( $query->tax_query ) && is_a( $query->tax_query, 'WP_Tax_Query' ) ) { 469 | // unfortunately there is no way to get table_aliases by native function 470 | // tax query does not have get_clauses either like meta query does 471 | // we have to find aliases the hard way 472 | $chunks = explode( 'JOIN', $join_sql ); 473 | 474 | // any join clauses? 475 | if ( ! empty( $chunks ) ) { 476 | $aliases = []; 477 | 478 | foreach ( $chunks as $chunk ) { 479 | // standard join 480 | if ( strpos( $chunk, $wpdb->prefix . 'term_relationships ON' ) !== false ) 481 | $aliases[] = $wpdb->prefix . 'term_relationships'; 482 | // alias join 483 | elseif ( strpos( $chunk, $wpdb->prefix . 'term_relationships AS' ) !== false && preg_match( '/' . $wpdb->prefix . 'term_relationships AS ([a-z0-9]+) ON/i', $chunk, $matches ) === 1 ) 484 | $aliases[] = $matches[1]; 485 | } 486 | 487 | // any aliases? 488 | if ( ! empty( $aliases ) ) { 489 | foreach ( array_unique( $aliases ) as $alias ) { 490 | $groupby[] = $alias . '.term_taxonomy_id'; 491 | } 492 | } 493 | } 494 | } 495 | 496 | return $groupby; 497 | } 498 | 499 | /** 500 | * Get meta table aliases from query. 501 | * 502 | * @param object $query 503 | * @return array 504 | */ 505 | private function get_groupby_meta_aliases( $query ) { 506 | $groupby = []; 507 | 508 | // valid query with meta query? 509 | if ( is_a( $query, 'WP_Query' ) && ! empty( $query->meta_query ) && is_a( $query->meta_query, 'WP_Meta_Query' ) ) { 510 | // get meta clauses, we can't use table_aliases here since it's protected value 511 | $clauses = $query->meta_query->get_clauses(); 512 | 513 | // any meta clauses? 514 | if ( ! empty( $clauses ) ) { 515 | $aliases = []; 516 | 517 | foreach ( $clauses as $clause ) { 518 | $aliases[] = $clause['alias']; 519 | } 520 | 521 | // any aliases? 522 | if ( ! empty( $aliases ) ) { 523 | foreach ( array_unique( $aliases ) as $alias ) { 524 | $groupby[] = $alias . '.meta_id'; 525 | } 526 | } 527 | } 528 | } 529 | 530 | return $groupby; 531 | } 532 | 533 | /** 534 | * Extend query object with total post views. 535 | * 536 | * @param array $posts 537 | * @param object $query 538 | * @return array 539 | */ 540 | public function the_posts( $posts, $query ) { 541 | if ( ( isset( $query->pvc_orderby ) && $query->pvc_orderby ) || ( isset( $query->pvc_query ) && $query->pvc_query ) || apply_filters( 'pvc_extend_post_object', false, $query ) === true ) { 542 | $sum = 0; 543 | 544 | // any posts found? 545 | if ( ! empty( $posts ) ) { 546 | foreach ( $posts as $post ) { 547 | if ( ! empty( $post->post_views ) ) 548 | $sum += (int) $post->post_views; 549 | } 550 | } 551 | 552 | // pass total views 553 | $query->total_views = $sum; 554 | } 555 | 556 | return $posts; 557 | } 558 | 559 | /** 560 | * Check whether date is valid. 561 | * 562 | * @param string $type 563 | * @param int $year 564 | * @param int $month 565 | * @param int $day 566 | * @param int $week 567 | * @return bool 568 | */ 569 | private function is_date_valid( $type, $year = 0, $month = 0, $day = 0, $week = 0 ) { 570 | switch ( $type ) { 571 | case 'y': 572 | $bool = ( $year >= 1 && $year <= 32767 ); 573 | break; 574 | 575 | case 'yw': 576 | $bool = ( $year >= 1 && $year <= 32767 && $week >= 0 && $week <= 53 ); 577 | break; 578 | 579 | case 'ym': 580 | $bool = ( $year >= 1 && $year <= 32767 && $month >= 1 && $month <= 12 ); 581 | break; 582 | 583 | case 'ymd': 584 | $bool = checkdate( $month, $day, $year ); 585 | break; 586 | 587 | case 'm': 588 | $bool = ( $month >= 1 && $month <= 12 ); 589 | break; 590 | 591 | case 'md': 592 | $bool = ( $month >= 1 && $month <= 12 && $day >= 1 && $day <= 31 ); 593 | break; 594 | 595 | case 'w': 596 | $bool = ( $week >= 0 && $week <= 53 ); 597 | break; 598 | 599 | case 'd': 600 | $bool = ( $day >= 1 && $day <= 31 ); 601 | break; 602 | } 603 | 604 | return $bool; 605 | } 606 | } 607 | --------------------------------------------------------------------------------