├── 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 |
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 | ';
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 |
204 |
205 |
217 |
';
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 |
--------------------------------------------------------------------------------