WP Meta SEO - Version v1.0.0

Version Description

Install new version over the old one

Download this release

Release Info

Developer JoomUnited
Plugin Icon 128x128 WP Meta SEO
Version v1.0.0
Comparing to
See all releases

Version v1.0.0

css/chart.css ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*-------------------------------------------------------
2
+ CSS FOR DASHBOARD
3
+ */
4
+ .dashboard{
5
+ float: left;
6
+ border: #CCC 1px solid;
7
+ width: 99%;
8
+ background: #FFF;
9
+ }
10
+
11
+ .dashboard .left{width: 50%; float: left;}
12
+ .dashboard .right{width: 50%; float: right; }
13
+ .avera{width: 100%; height: 40px; font-size: 24px ;position: absolute; top: 50%; left: 0; margin-top: -20px; line-height:19px; text-align: center; z-index: 999999999999999}
14
+
15
+ .title-seo {
16
+ font-weight: bold;
17
+ }
18
+
19
+ .load {
20
+ height: 20px;
21
+ background: #4a8ace;
22
+ -moz-border-radius: 2px;
23
+ border-radius: 2px;
24
+ }
25
+
26
+ .noload {
27
+ float: right;
28
+ width: 60%;
29
+ background: #cecfce;
30
+ -moz-border-radius: 2px;
31
+ border-radius: 2px;
32
+ border: 1px solid #eee;
33
+ position: relative;
34
+ margin-top: -22px;
35
+ }
36
+
37
+ .loadtext {
38
+ font-family: Consolas;
39
+ font-size: 14px;
40
+ color: #FFF;
41
+ padding-left: 3px;
42
+ position: absolute;
43
+ bottom: 1px;
44
+ left: 40%;
45
+ }
46
+
47
+ .dashboard h1 {
48
+ padding: 10px 0px 10px 20px
49
+ }
50
+
51
+ .dashboard-left {
52
+ margin: 10px;
53
+ min-height:580px;
54
+ background: #eff3f7;
55
+ padding-bottom: 10px;
56
+ }
57
+
58
+ #canvas-holder{
59
+ margin:50px 0 0 0;
60
+ width:100%;
61
+ }
62
+ #chart-container {
63
+ margin: 10px 10px 0 10px;
64
+ padding: 10px;
65
+ background-color: #fff;
66
+ }
67
+
68
+ #canvas-holder h2 {
69
+ text-align: center;
70
+ }
71
+
72
+ .dashboard-right {
73
+ padding-bottom: 10px;
74
+ margin-top: 10px;
75
+ position: relative;
76
+ }
77
+ #wpmetaseo-update-version {
78
+ bottom: 0;
79
+ width: 90% ;
80
+ position: absolute;
81
+ }
82
+ #alexa-ranking{
83
+ float:left;
84
+ margin-left:30px;
85
+ margin-top: 30px;
86
+ }
87
+ #wpmetaseo-update-version {
88
+ background: none repeat scroll 0 0 #eff3f7;
89
+ padding:20px;
90
+ }
91
+ #wpmetaseo-update-version h4 {
92
+ margin:0;
93
+ }
94
+ #wpmetaseo-update-version ul {
95
+ margin-bottom:0;
96
+ }
97
+
98
+ header{
99
+ width: 90%;
100
+ margin: 0px auto;
101
+ padding: 30px 10px 10px 20px;
102
+ }
css/google-chart-dashboard.css ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .form-left{
2
+ width: 450px;
3
+ border-radius: 10px;
4
+ border: #CCC 1px solid;
5
+ margin: 20px 0px 0px 20px;
6
+ float: left;
7
+ padding: 20px 35px 20px 35px;
8
+ position: relative;
9
+ }
10
+
11
+ .form-right{
12
+ width: 450px;
13
+ border-radius: 10px;
14
+ border: #CCC 1px solid;
15
+ margin: 20px 20px 0px 0px;
16
+ float: right;
17
+ padding: 20px 35px 20px 35px;
18
+ position: relative;
19
+ }
20
+ .form-left .report{width: 100%; height: 300px}
21
+ .form-right .report{width: 100%; height: 300px}
22
+ .form-left .title{position: absolute; left: 30px; top:-10px; font-weight: bold; background:#eff3ef; padding: 0px 20px 0px 20px }
23
+ .form-right .title{position: absolute; left: 30px; top:-10px; font-weight: bold; background:#eff3ef; padding: 0px 20px 0px 20px }
css/metaseo_admin.css ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ To change this license header, choose License Headers in Project Properties.
3
+ To change this template file, choose Tools | Templates
4
+ and open the template in the editor.
5
+ */
6
+ /*
7
+ Created on : Sep 26, 2014, 11:41:40 AM
8
+ Author : Vien Nguyen
9
+ */
10
+
11
+ .search-box{padding: 0px 0px 20px 0px; float: left}
12
+ .widefat .column-col_id {
13
+ width: 2.2em;
14
+ vertical-align: top;
15
+ }
16
+ .column-col_meta_title {
17
+ width: 20%;
18
+ }
19
+ .column-col_meta_desc {
20
+ width: 30%;
21
+ }
22
+
23
+ .title-len, .desc-len, .word-exceed {
24
+ color: #FFF;
25
+ border-radius: 2px;
26
+ min-width: 15px;
27
+ text-align: center;
28
+ float: left;
29
+ padding: 0px 10px;
30
+ position: relative;
31
+ bottom: 12px;
32
+ left: 15px;
33
+ }
34
+
35
+ .title-len, .desc-len {
36
+ background: none repeat scroll 0% 0% #7ad03a;
37
+
38
+ }
39
+ .word-exceed {
40
+ background: none repeat scroll 0% 0% #FFCC00;
41
+ }
42
+
43
+ .savedInfo {
44
+ float: right;
45
+ color: #999;
46
+ margin-right: 30px;
47
+ }
48
+
49
+ /* Snippet */
50
+ .snippet {
51
+ border:none;
52
+ padding: 4px;
53
+ margin-top: 2px;
54
+ }
55
+
56
+ .snippet_metatitle{
57
+ display: block;
58
+ color: #1a0dab;
59
+ font-size: 16px!important;
60
+ line-height: 1.2;
61
+ text-overflow: ellipsis;
62
+ }
63
+ .snippet_metalink {
64
+ color: #006621;
65
+ font-size: 13px;
66
+ line-height: 16px;
67
+ }
68
+
69
+ .widefat .snippet_metades {
70
+ color: #545454;
71
+ }
72
+
73
+ a.snippet_metatitle:hover {
74
+ text-decoration: underline;
75
+ color: #1E0FBE;
76
+ }
77
+
78
+ a.info-content {
79
+ outline:none;
80
+ padding-right: 10px;
81
+ }
82
+
83
+ a.info-content:hover {
84
+ text-decoration:none;
85
+ }
86
+
87
+ a.info-content .tooltip-metacontent {
88
+ z-index:10;
89
+ display:none;
90
+ padding:10px 15px;
91
+ margin-top:3px;
92
+ margin-left:10px;
93
+ width:250px;
94
+ }
95
+
96
+ a.info-content img {
97
+ height: 13px;
98
+ }
99
+
100
+ a.info-content .tooltip-metacontent .tooltip-metatitle {
101
+ overflow: hidden;
102
+ width: 512px;
103
+ color: #1e0fbe;
104
+ font-size: 18px!important;
105
+ line-height: 1.2;
106
+ white-space: nowrap;
107
+ text-overflow: ellipsis;
108
+ }
109
+
110
+ a.info-content .tooltip-metacontent .tooltip-metalink{color: #006621;
111
+ font-size: 13px;
112
+ line-height: 16px;
113
+ }
114
+
115
+ a.info-content .tooltip-metacontent .tooltip-metades {
116
+ color: #777
117
+ }
118
+
119
+ a.info-content:hover .tooltip-metacontent {
120
+ display:inline;
121
+ position:absolute;
122
+ color:#777;
123
+ border:1px solid #CCC;
124
+ border-radius: 2px;
125
+ background:#ffffff;
126
+ }
127
+
128
+ a.info-content .tooltip-metacontent
129
+ {
130
+ border-radius:2px;
131
+ box-shadow: 5px 5px 8px #eee;
132
+
133
+ }
134
+
135
+ .metaseo-metatitle, .metaseo-metadesc{
136
+ padding:4px 6px;
137
+ }
138
+ .action-wrapper{
139
+ width:38%;
140
+ float:left;
141
+ }
142
+ .snippet-wrapper{
143
+ width:60%;
144
+ float:left;
145
+ margin-left:2%
146
+ }
147
+
148
+ @media all and (max-width:768px){
149
+ .action-wrapper, .snippet-wrapper{
150
+ width: auto; float:none;
151
+ }
152
+ }
153
+
154
+ /* Alexa CSS */
155
+ .rank-row, .metrics-row {
156
+ margin-bottom: 25px;
157
+ margin-top: 0px !important;
158
+ }
159
+ a.tt {
160
+ cursor: help;
161
+ }
162
+ .ptt {
163
+ left: 57px;
164
+ position: absolute;
165
+ top: 210px;
166
+ }
167
+ .tt {
168
+ color: #333;
169
+ font-size: 12px !important;
170
+ font-weight: normal !important;
171
+ text-align: left;
172
+ text-decoration: none !important;
173
+ }
174
+ .tt span {
175
+ display: none;
176
+ }
177
+ .tt .fakeLink {
178
+ text-decoration: underline;
179
+ }
180
+ .tt img {
181
+ vertical-align: baseline;
182
+ }
183
+ .ptt-hover, .tt:hover {
184
+ background: none repeat scroll 0 0 transparent;
185
+ color: #aaaaff;
186
+ position: relative;
187
+ text-decoration: none;
188
+ z-index: 10000;
189
+ }
190
+ .ptt-hover span.tttooltip, .tt:hover span.tttooltip {
191
+ color: #111;
192
+ display: block;
193
+ left: -60px;
194
+ padding: 15px 0 0;
195
+ position: absolute;
196
+ text-align: left;
197
+ text-decoration: none;
198
+ top: -10px;
199
+ white-space: normal !important;
200
+ width: 200px;
201
+ z-index: 20000;
202
+ }
203
+ .ptt-hover.rightEnd span.tttooltip, .tt.rightEnd:hover span.tttooltip {
204
+ left: auto;
205
+ right: -20px;
206
+ }
207
+ .ptt-hover span.top, .tt:hover span.top {
208
+ background: url("../img/bubble-top-v2.png") no-repeat scroll 80px 1px rgba(0, 0, 0, 0);
209
+ display: block;
210
+ padding: 15px 8px 0;
211
+ }
212
+ .ptt-hover.rightEnd span.top, .tt.rightEnd:hover span.top {
213
+ background: url("/images/help/bubble-right.png") no-repeat scroll center top rgba(0, 0, 0, 0);
214
+ }
215
+ .ptt-hover span.middle, .tt:hover span.middle {
216
+ background-color: #f2f2f2;
217
+ border: 1px solid #c5c5c5;
218
+ border-radius: 3px;
219
+ display: block;
220
+ line-height: normal;
221
+ padding: 8px;
222
+ }
223
+ .ptt-hover span.bottom, .tt:hover span.bottom {
224
+ color: #548912;
225
+ display: block;
226
+ padding: 0 8px;
227
+ }
228
+ .popover {
229
+ border: 1px solid #3b81de;
230
+ box-shadow: none;
231
+ }
232
+ .popover, .popover-title, .popover a, .popover a:visited, .popover a:hover, .popover a:active {
233
+ color: #fff;
234
+ }
235
+ .siteInfoPage h3.popover-title {
236
+ color: #fff;
237
+ }
238
+ .popover-title {
239
+ border-bottom: 0 none;
240
+ font-size: 12px;
241
+ font-weight: bold;
242
+ }
243
+ .popover, .popover-title {
244
+ background-color: #3b81de;
245
+ }
246
+ .popover-content {
247
+ padding-top: 0;
248
+ }
249
+ .popover.top {
250
+ margin-top: -10px;
251
+ }
252
+ .popover.bottom {
253
+ margin-top: 10px;
254
+ }
255
+ .popover.left {
256
+ margin-left: -20px;
257
+ }
258
+ .popover.right {
259
+ margin-left: 20px;
260
+ }
261
+ .popover.right .arrow, .popover.right .arrow:after {
262
+ border-right-color: #3b81de;
263
+ }
264
+ .popover.left .arrow, .popover.left .arrow:after {
265
+ border-left-color: #3b81de;
266
+ }
267
+ .popover.top .arrow, .popover.top .arrow:after {
268
+ border-top-color: #3b81de;
269
+ }
270
+ .popover.bottom .arrow, .popover.bottom .arrow:after {
271
+ border-bottom-color: #3b81de;
272
+ }
273
+
274
+ .spark-bars {
275
+ display: none;
276
+ }
277
+ .panel-wrapper.no-padding .traffic-rankpromo-content {
278
+ padding: 12px 20px;
279
+ }
280
+
281
+ .img-inline {
282
+ padding-right: 5px;
283
+ vertical-align: middle;
284
+ }
285
+
286
+ .align-vmiddle {
287
+ vertical-align: middle;
288
+ }
289
+ .metrics-data, .metrics-pending {
290
+ color: #333;
291
+ font-family: "Open Sans",Helvetica,Arial,sans-serif;
292
+ font-size: 24px;
293
+ }
294
+ strong {
295
+ font-weight: bold;
296
+ }
297
+ .change-wrapper {
298
+ border-radius: 3px;
299
+ color: #fff;
300
+ font-family: "Open Sans",Helvetica,Arial,sans-serif;
301
+ font-size: 12px;
302
+ margin: 0;
303
+ padding: 3px 5px 3px 15px;
304
+ }
305
+ .change-up.change-r {
306
+ background: url("../img/arrow-up.png") no-repeat scroll 5px 50% #c32f16;
307
+ color: #fff;
308
+ }
309
+ .align-vmiddle {
310
+ vertical-align: middle;
311
+ }
312
+ .change-up {
313
+ background: url("../img/arrow-up.png") no-repeat scroll 5px 50% #68b325;
314
+ color: #fff;
315
+ }
316
+
317
+ .change-down {
318
+ background: #68b325 url("../img/arrow-down.png") no-repeat 5px 50%;
319
+ color: #fff;
320
+ }
321
+
322
+ .no-wrap {
323
+ white-space: nowrap;
324
+ }
325
+
326
+ .metrics-title {
327
+ color: #666;
328
+ font-weight: normal;
329
+ margin: 0 0 10px;
330
+ }
331
+ .note-no-data{
332
+ padding:12px 12px 12px 0;
333
+ }
334
+ .yellow-box {
335
+ height: auto;
336
+ padding: 5px 10px;
337
+ }
338
+ .yellow-box {
339
+ background-color: #fdf9e0;
340
+ border: 1px solid #feed78;
341
+ }
342
+ .note-no-data .col-pad {
343
+ background: url("../img/no-data-alert.png") no-repeat scroll 0 50% transparent;
344
+ display: block;
345
+ padding-left: 45px;
346
+ }
css/style.css ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .search-box{padding: 0px 0px 20px 0px; float: left}
2
+ .img_advan{border: #CCC 1px solid;border-radius: 0px 30px 30px 0px}
3
+ .listpre img{float: left; padding: 5px}
4
+ .listpre:hover>.show-popup{display: block;}
5
+
6
+ a.show-popup {
7
+ display: none;
8
+ margin:10px 0 0 0;
9
+ float:left;
10
+
11
+ }
12
+ div.popup-bg {
13
+ position:absolute;
14
+ top:0;
15
+ bottom:0;
16
+ left:-20px;
17
+ right:0;
18
+ z-index:99;
19
+ background:#999;
20
+ display:none;
21
+ }
22
+ div.popup {
23
+ border-radius:2px;
24
+ z-index:999;
25
+ display:none;
26
+ background:#FFF;
27
+ overflow: auto;
28
+ }
29
+ div.popup-header {
30
+ position:relative;
31
+ float:left;
32
+ width:1000px;
33
+ line-height:30px;
34
+ background:#FF9933;
35
+ }
36
+
37
+ .popup-header .pop-title{
38
+ background: #FFF;
39
+ padding: 6px 0px 0px 15px;
40
+ color:#555!important;
41
+ margin:0;
42
+ font-size:16px;
43
+ }
44
+ span.popup-close {
45
+ cursor:pointer;
46
+ color: #555;
47
+ font-weight: bold;
48
+ font-size:16px;
49
+ position:absolute;
50
+ right:10px;
51
+ z-index:9999;
52
+ }
53
+ div.popup-content .content-header{
54
+ color:#666;margin:0;padding: 10px 10px 12px 14px;background: #fffeee
55
+ }
56
+ div.popup-content .content-box{
57
+ /*width:980px;*/
58
+ min-height:250px;
59
+ overflow: auto;
60
+ padding: 0 10px 10px;
61
+ position: relative;
62
+ }
63
+
64
+ span.spinner-light{
65
+ background: url("../../../../wp-admin/images/wpspin_light.gif") no-repeat scroll 0 0 / 16px 16px rgba(0, 0, 0, 0);
66
+ background-size: 16px 16px;
67
+ display: none;
68
+ float: right;
69
+ opacity: 0.7;
70
+ filter: alpha(opacity=70);
71
+ width: 16px;
72
+ height: 16px;
73
+ margin: 5px 5px 0;
74
+ position: absolute; right:-8px;top:-19px; z-index: 999;
75
+ }
76
+ span.metaseo-checked{
77
+ background: url("../../../../wp-admin/images/yes.png") no-repeat scroll 0 0 / 20px 20px rgba(0, 0, 0, 0);
78
+ display: inline-block;
79
+ float: right;
80
+ height: 20px;
81
+ margin: 2px 5px 0;
82
+ opacity: 0.7;
83
+ width: 20px;
84
+ }
85
+ p.metaseo-msg{
86
+ background: none repeat scroll 0 0 #fff;
87
+ border-left: 4px solid #7ad03a;
88
+ box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.1);
89
+ bottom: 0;
90
+ clear: both;
91
+ color: #444;
92
+ display: none;
93
+ margin: 5px !important;
94
+ padding: 3px 5px;
95
+ right: 2px;
96
+ text-align: center;
97
+ }
98
+ .metaseo-border-top td{
99
+ border-top:1px solid #ddd;
100
+ }
101
+
102
+ .metaseo-border-bottom td{
103
+ border-bottom:1px solid #dedede;
104
+ }
105
+ td.metaseo-action{
106
+ text-align:center;
107
+ }
108
+ .metaseo-img-wrapper{
109
+ clear:left;
110
+ padding:5px;
111
+ }
112
+ .metaseo-img-wrapper div.dimension{
113
+ display:inline-block;padding:5px;
114
+ font-size:12px;color:#555;
115
+ background:none;
116
+ opacity: 0.9;
117
+ filter: alpha(opacity=90);
118
+ clear:left;
119
+ }
120
+ .metaseo-img-wrapper div.dimension span{
121
+ font-weight:bold;
122
+ }
123
+ .metaseo-img-wrapper img, .metaseo-img img{
124
+ max-width:100%;
125
+ }
126
+ .metaseo-img-wrapper .metaseo-img{
127
+ position:relative;
128
+ }
129
+
130
+ .metaseo-img .img-choosen{
131
+ position: absolute;
132
+ top:0px;
133
+ left: 0px;
134
+ float:left;
135
+ }
136
+
137
+ div.img-name-wrapper{
138
+ font-size:0.98em;
139
+ }
140
+
141
+ div.img-name-wrapper p{
142
+ margin:3px;padding-left:2px;
143
+ }
144
+
145
+ table.metaseo_images td input{
146
+ font-size:0.99em!important;
147
+ color:#686868;
148
+ max-width:100%;
149
+ }
150
+
151
+ a.button.meta-default{
152
+ float: left;
153
+ border: #CCC 1px solid;
154
+ color: #018601;
155
+ width: 70px;
156
+ font-weight: bold;
157
+ text-align: center;
158
+ padding-top: 4px;
159
+ line-height: 16px;
160
+ }
161
+
162
+ a.button.meta-default img {
163
+ width: 14px;
164
+ height: 8px;
165
+ }
166
+
167
+ .img_seo_type {
168
+ padding: 5px;
169
+ float: left
170
+ }
171
+
172
+ input.metaseo-fix-meta {
173
+ float:left;
174
+ margin-right:6px;
175
+ }
176
+
177
+ /* Tooltip */
178
+ a.info {outline:none; padding-right: 10px}
179
+
180
+ a.info:hover {text-decoration:none;}
181
+
182
+ a.info .tooltip{
183
+ z-index:10;display:none; padding:14px 20px;
184
+ margin-top:-60px; margin-left:10px;
185
+ width:250px; line-height:15px;
186
+ }
187
+
188
+ a.info:hover .tooltip {
189
+ display:inline;
190
+ position:absolute;
191
+ color:#111;
192
+ border:1px solid #DCA;
193
+ border-radius: 10px;
194
+ background:rgba(51,51,51,0.8);
195
+ color:#ffffff;
196
+ font-style: italic;
197
+ }
198
+
199
+ a.info .tooltip
200
+ {
201
+ border-radius:4px;
202
+ box-shadow: 5px 5px 8px #CCC;
203
+ }
204
+
205
+ .opt-info-warning{
206
+ background:url('../img/warning-25x25.png') no-repeat scroll 1px 5px;
207
+ min-height:30px;
208
+ }
209
+
210
+ .opt-info{
211
+ padding-left:30px;
212
+ }
213
+
214
+ span.saved-info {
215
+ display: block;
216
+ margin: 0;
217
+ min-height: 18px;
218
+ padding-left: 5px;
219
+ clear:left;
220
+ }
221
+
222
+ .metaseo-msg-success {
223
+ color:#7ad03a!important;
224
+ }
225
+
226
+ .metaseo-msg-warning{
227
+ color:#f51212!important;
228
+ }
229
+
230
+ .opt-info > p, .opt-info-warning > p {
231
+ font-size:0.95em!important;
232
+ color:#2EA2CC;
233
+ margin-bottom:1px!important;
234
+ }
235
+
236
+ a.img-resize, a.fix-metas{
237
+ font-size:11px!important;
238
+ margin-top:8px!important;
239
+ position: relative;
240
+ }
241
+ a.fix-metas, a.img-resize{
242
+ float:right;
243
+ margin-left:8px!important;
244
+ }
245
+
246
+ td.meta-info-row{
247
+ position:relative;
248
+ }
249
+ th.column-col_image_info{
250
+ padding-left:35px;
251
+ }
252
+
253
+ span.metaseo-loading{
254
+ display:block;width:32px;height:32px;border-radius:32px;
255
+ position:absolute;left:45%;top:40%;
256
+ background:url('../img/gplus-loader.gif') no-repeat scroll 0 0 transparent;
257
+ }
258
+ span.meta-update{
259
+ float:left;
260
+ display:inline-block;
261
+ width:30px;
262
+ height:30px;
263
+ }
264
+ span.update-loader{
265
+ background: url('../img/update_loader.gif') no-repeat scroll 0 0 transparent;
266
+ }
267
+
268
+ span.hz-loading{
269
+ background: url('../img/hz-loading.gif') no-repeat scroll 8px 6px transparent;
270
+ display:inline-block;
271
+ min-width:100px;
272
+ min-height:14px;
273
+ }
274
+
css/tooltip-metaimage.css ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ a.info {outline:none; padding-right: 10px}
2
+ a.info:hover {text-decoration:none;}
3
+ a.info .tooltip{
4
+ z-index:10;display:none; padding:14px 20px;
5
+ margin-top:-60px; margin-left:10px;
6
+ width:250px; line-height:15px;
7
+ }
8
+ a.info:hover .tooltip {
9
+ display:inline; position:absolute; color:#111;
10
+ border:1px solid #DCA;
11
+ border-radius: 10px;
12
+ background:rgba(51,51,51,0.8);
13
+ color:#ffffff;
14
+ font-style: italic;
15
+ }
16
+ a.info .tooltip
17
+ {
18
+ border-radius:4px;
19
+ box-shadow: 5px 5px 8px #CCC;
20
+ }
css/tooltip.css ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ a.info {outline:none; padding-right: 10px}
2
+ a.info:hover {text-decoration:none;}
3
+ a.info .tooltip {
4
+ z-index:10;display:none; padding:14px 20px;
5
+ margin-top:-65px; margin-right:28px;
6
+ width:250px; line-height:16px;
7
+ }
8
+ a.info:hover .tooltip{
9
+ display:inline; position:absolute; color:#111;
10
+ border:1px solid #DCA;
11
+ border-radius: 10px;
12
+ background:rgba(51,51,51,0.8);
13
+ color:#ffffff;
14
+ font-style: italic;
15
+ }
16
+ a.info .tooltip
17
+ {
18
+ border-radius:4px;
19
+ box-shadow: 5px 5px 8px #CCC;
20
+ }
img/ajax-loader.gif ADDED
Binary file
img/arrow-down.png ADDED
Binary file
img/arrow-up.png ADDED
Binary file
img/bubble-top-v2.png ADDED
Binary file
img/gplus-loader.gif ADDED
Binary file
img/hz-loading.gif ADDED
Binary file
img/icon.png ADDED
Binary file
img/img-arrow.png ADDED
Binary file
img/info.png ADDED
Binary file
img/no-data-alert.png ADDED
Binary file
img/update_loader.gif ADDED
Binary file
img/update_loading.gif ADDED
Binary file
img/view.gif ADDED
Binary file
img/view.png ADDED
Binary file
img/warning-20x20.png ADDED
Binary file
img/warning-25x25.png ADDED
Binary file
img/warning.png ADDED
Binary file
inc/class.image-helper.php ADDED
@@ -0,0 +1,513 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class ImageHelper{
4
+ /**
5
+ *
6
+ */
7
+ public static $src = '';
8
+
9
+ public function __construct(array $params = array()){
10
+ $c = new ReflectionClass(__CLASS__);
11
+ $statics = $c->getStaticProperties();
12
+
13
+ foreach($params as $key => $value){
14
+ if(!in_array($key, array_keys($statics))){
15
+ $this->{$key} = $value;
16
+ }
17
+ else{
18
+ self::$$key = $value;
19
+ }
20
+ }
21
+ }
22
+
23
+ /**
24
+ *
25
+ */
26
+ public static function IGetPart($src = ''){
27
+ if($src == '') { $src = self::$src; }
28
+ $ipart = new ImageHelper(array(
29
+ 'src' => $src,
30
+ 'base_path' => '',
31
+ 'name' => '',
32
+ 'ext' => '',
33
+ 'dimension' => '',
34
+ 'name_prefix' => '',
35
+ 'name_suffix' => ''));
36
+
37
+ if(($dot = strrpos($src, '.')) === false){
38
+ return $ipart;
39
+ }
40
+
41
+ if(($bslash = strrpos($src, '/')) === false){
42
+ $bslash = 0;
43
+ }
44
+ else{ $bslash++; }
45
+
46
+ $ipart->_src = $src;
47
+ $ipart->base_path = substr($src, 0, $bslash);
48
+ $ipart->name = substr($src, $bslash, $dot - $bslash);
49
+ $ipart->ext = substr($src, $dot);
50
+ $ipart->dimension = self::IGetDimension($ipart->name);
51
+ if(!empty($ipart->dimension)){
52
+ $ipart->name_prefix = substr($ipart->name,0,strpos( $ipart->name,$ipart->dimension ) - 1 );
53
+ $ipart->name_suffix = str_replace($ipart->name_prefix.'-'.$ipart->dimension, '', $ipart->name);
54
+ }else {
55
+ $ipart->name_prefix = $ipart->name_suffix = '';
56
+ }
57
+
58
+ return $ipart;
59
+ }
60
+
61
+ /**
62
+ *
63
+ */
64
+ public static function IGetDimension($src){
65
+ if(preg_match_all('/(\d+)x(\d+)/i', $src, $match)){
66
+ return count($match) > 0 ? end($match[0]) : '';
67
+ }
68
+
69
+ return '';
70
+ }
71
+
72
+ /**
73
+ *
74
+ */
75
+ public static function get($part, $src = ''){
76
+ if($src == ''){ $src = self::$src; }
77
+ $ipart = self::getIPart($src);
78
+ return isset($ipart->{$part}) ? $ipart->{$part} : '';
79
+ }
80
+
81
+ /**
82
+ *
83
+ */
84
+ public static function IReplace($newName, $src = ''){
85
+ if($src == ''){ $src = self::$src; }
86
+ if($newName === ''){
87
+ return $src;
88
+ }
89
+
90
+ $ipart = self::IgetPart($src);
91
+ $src = $ipart->base_path.$newName.'-'.$ipart->dimension.$ipart->name_suffix.$ipart->ext;
92
+ return $src;
93
+ }
94
+
95
+ /**
96
+ *
97
+ */
98
+ public static function IHasClone($img, $obj){
99
+ if(!is_array($img)){ $img = array($img); }
100
+ $obj_part = self::IGetPart($obj);
101
+
102
+ foreach($img as $_img){
103
+ $img_part = self::IGetPart($_img);
104
+ if($img_part->ext !== $obj_part->ext){
105
+ continue;
106
+ }
107
+
108
+ if($obj_part->dimension !== ''){
109
+ $cmp = $obj_part->name_prefix;
110
+ }else{ $cmp = $obj_part->name; }
111
+
112
+ if( strcmp( $img_part->name, $cmp ) === 0 ){
113
+ return $_img;
114
+ }
115
+ }
116
+
117
+ return false;
118
+ }
119
+
120
+ /**
121
+ *
122
+ */
123
+ public static function IScan($imgs = array(), $content){
124
+ $ifound = array();
125
+ $doc = new DOMDocument();
126
+ @$doc->loadHtml($content);
127
+ $tags = $doc->getElementsByTagName('img');
128
+
129
+ //For standard images names, convert spaces to -
130
+ $_imgs = array();
131
+ foreach($imgs as $iname => $iid){
132
+ $iname = preg_replace('/(\s{1,})/', '-', $iname);
133
+ $_imgs[$iname] = $iid;
134
+ }
135
+
136
+ if($tags->length > 0){
137
+ foreach($tags as $order => $tag){
138
+ if(($obj = $tag->getAttribute('src')) == '') { continue; }
139
+ if($img_name = self::IHasClone(array_keys($_imgs), $obj)){
140
+ if(!empty($_imgs[$img_name])){
141
+ $ifound[$order]['id'] = $_imgs[$img_name];
142
+ $ifound[$order]['src'] = $obj;
143
+ $ifound[$order]['width'] = $tag->getAttribute('width');
144
+ $ifound[$order]['height'] = $tag->getAttribute('height');
145
+ $ifound[$order]['alt'] = trim($tag->getAttribute('alt'));
146
+ $ifound[$order]['title'] = trim($tag->getAttribute('title'));
147
+ }
148
+ else{ continue; }
149
+ }
150
+ }
151
+ }
152
+
153
+ return $ifound;
154
+ }
155
+
156
+ public static function _get_post_list($iID, $opt_type){
157
+ global $wpdb;
158
+ //Get image info from wp_postmeta with key equals to _metaseo_img_meta_not_good
159
+ $meta_key = '_metaseo_' . strtolower(trim($opt_type));
160
+ $posts = get_post_meta($iID, $meta_key, true);
161
+
162
+ if(is_array($posts) && !empty($posts)){
163
+ $posts = metaseo_utf8($posts, 'decode');
164
+ return $posts;
165
+ }
166
+
167
+ return array();
168
+ }
169
+
170
+ /**
171
+ *
172
+ */
173
+ public static function IPrepare($imgs, $posts, $meta_checkout = array(), $forceScan = false){
174
+ $iNotGood = array();
175
+ $iNotGoodTotal = array();
176
+ $metaNotGood = array();
177
+ $metaNotGoodTotal = array();
178
+ $upload_dir = wp_upload_dir();
179
+
180
+ foreach($posts as $post){
181
+ if(empty($post->post_content)) { continue; }
182
+ $ifound = self::IScan($imgs, $post->post_content);
183
+ if(count($ifound) < 1){ continue; }
184
+
185
+ foreach($ifound as $order => $img){
186
+ $iID = $img['id'];
187
+ //Get image that its size is not good
188
+ if(!list($width_origin, $height_origin) = @getimagesize($img['src'])){ continue; }
189
+ $ratio_origin = $width_origin/$height_origin;
190
+ $width = $img['width'];
191
+ $height = $img['height'];
192
+ //Check if img tag is missing with/height attribute value or not
193
+ if(!$width && !$height){
194
+ $width = $width_origin;
195
+ $height = $height_origin;
196
+ }
197
+ elseif($width && !$height){
198
+ $height = $width*(1/$ratio_origin);
199
+ }
200
+ elseif($height && !$width){
201
+ $width = $height*($ratio_origin);
202
+ }
203
+
204
+ if($width_origin > $width || $height_origin > $height){
205
+ $img_before = str_replace(array($upload_dir['baseurl']),'',$img['src']);
206
+ $ibpart = ImageHelper::IGetPart($img_before);
207
+
208
+ $img_after = $ibpart->base_path
209
+ .(!empty($ibpart->name_prefix) ? $ibpart->name_prefix : $ibpart->name)
210
+ . '-' . $width . 'x' . $height
211
+ . $ibpart->name_suffix
212
+ . $ibpart->ext;
213
+
214
+ $destination = $upload_dir['basedir'] . '/' . $img_after;
215
+
216
+ $srcs = array(
217
+ 'img_before_dir' => $upload_dir['basedir'] . $img_before,
218
+ 'img_before_url' => $upload_dir['baseurl'] . $img_before,
219
+ 'img_after_dir' => $upload_dir['basedir'] . $img_after,
220
+ 'img_after_url' => $upload_dir['baseurl'] . $img_after
221
+ );
222
+ $size = (filesize($srcs['img_before_dir'])/1024);
223
+ if($size>1024){
224
+ $size=$size/1024;
225
+ $sizes = 'MB';
226
+ }else{
227
+ $sizes = 'KB';
228
+ }
229
+ $size=@round($size,1);
230
+
231
+ $iNotGood[$iID][$post->ID]['ID'] = $post->ID;
232
+ $iNotGood[$iID][$post->ID]['title'] = $post->post_title;
233
+ $iNotGood[$iID][$post->ID]['post_type'] = $post->post_type;
234
+ $iNotGood[$iID][$post->ID]['img_before_optm'][$order] = array(
235
+ 'size' => $size,
236
+ 'sizes' => $sizes,
237
+ 'src' => $srcs['img_before_url'],
238
+ 'width' => $width,
239
+ 'height' => $height,
240
+ 'dimension' => ImageHelper::IGetDimension($img_before)
241
+ );
242
+
243
+ $iNotGood[$iID][$post->ID]['img_after_optm'][$order] = array(
244
+ 'size' => 0,
245
+ 'path' => $img_after,
246
+ 'src' => $srcs['img_after_url'],
247
+ 'src_origin' => $srcs['img_before_url'],
248
+ 'width' => $width,
249
+ 'height' => $height,
250
+ 'dimension' => ImageHelper::IGetDimension($img_after)
251
+ );
252
+
253
+ //Get the number of images which their size are not good
254
+ if(!isset($iNotGoodTotal[$iID])){
255
+ $iNotGoodTotal[$iID] = 0;
256
+ }
257
+ $iNotGoodTotal[$iID]++;
258
+
259
+ }else{
260
+ if(!isset($iNotGood[$iID])){ $iNotGood[$iID] = array(); }
261
+ if(!isset($iNotGoodTotal[$iID])){ $iNotGoodTotal[$iID] = 0; }
262
+ }
263
+
264
+
265
+ //Get image that its meta/metas is/are not good
266
+ if(count($meta_checkout) > 0){
267
+ foreach($meta_checkout as $key => $meta){
268
+ $meta_value = $img[$meta];
269
+ $imNotGood[$iID][$post->ID]['ID'] = $post->ID;
270
+ $imNotGood[$iID][$post->ID]['title'] = $post->post_title;
271
+ $imNotGood[$iID][$post->ID]['post_type'] = $post->post_type;
272
+ $imNotGood[$iID][$post->ID]['meta'][$order]['img_src'] = $img['src'];
273
+ $imNotGood[$iID][$post->ID]['meta'][$order]['type'][$meta] = $meta_value;
274
+
275
+ #if($forceScan !== TRUE && $meta_value != ''){
276
+ #unset($_posts['meta_not_good'][$img_post_id][$post->ID]['meta'][$order]['type'][$meta]);
277
+ #}
278
+
279
+ if($meta_value == ''){
280
+ if(!isset($imNotGoodTotal[$iID][$meta])){
281
+ $imNotGoodTotal[$iID][$meta] = 0;
282
+ }
283
+ $imNotGoodTotal[$iID][$meta]++;
284
+ }
285
+
286
+ }
287
+ }
288
+ }
289
+
290
+ }
291
+
292
+ foreach($imgs as $name => $iID){
293
+ if(!isset($iNotGoodTotal[$iID])){ $iNotGoodTotal[$iID] = -1; }
294
+ if(!isset($iNotGood[$iID])){ $iNotGood[$iID] = array(); }
295
+ if(!isset($imNotGood[$iID])){ $imNotGood[$iID] = array(); }
296
+ if(!isset($imNotGoodTotal[$iID])){
297
+ foreach($meta_checkout as $mkey){
298
+ $imNotGoodTotal[$iID][$mkey] = 0;
299
+ }
300
+ }
301
+ }
302
+
303
+ foreach($imNotGoodTotal as &$mStatis){
304
+ foreach($meta_checkout as $mkey){
305
+ if(!isset($mStatis[$mkey])){ $mStatis[$mkey] = 0; }
306
+ }
307
+ }
308
+
309
+ unset($posts, $imgs);
310
+
311
+ $ret = array('iNotGood' => $iNotGood, 'iNotGoodTotal' => $iNotGoodTotal, 'imNotGood' => $imNotGood, 'imNotGoodTotal' => $imNotGoodTotal);
312
+
313
+ return $ret;
314
+ }
315
+
316
+ /*
317
+ * Scan image that has not good size or not good meta(s), then update info to postmeta table
318
+ * @return: array of message that will be responsed to ajax call
319
+ */
320
+ public static function IScanPosts($imgs, $meta_checking = false, $forceScan = false){
321
+ global $wpdb;
322
+ $msg = array();
323
+ $meta_keys = array('alt', 'title');
324
+ $_imgs = array_flip($imgs);
325
+
326
+ $post_types = MetaSeo_Content_List_Table::get_post_types();
327
+ $query = "SELECT `ID`, `post_title`, `post_content`, `post_type`, `post_date`
328
+ FROM $wpdb->posts
329
+ WHERE `post_type` IN ($post_types)
330
+ AND `post_content` <> ''
331
+ AND `post_content` LIKE '%<img%>%'
332
+ ORDER BY ID";
333
+
334
+ $posts = $wpdb->get_results($query);
335
+ $results = self::IPrepare($imgs, $posts, $meta_keys, $forceScan);
336
+
337
+ //Update some value into fields in wp_postmeta
338
+ if(count($results['iNotGood']) > 0){
339
+ foreach($results['iNotGood'] as $iID => $post_group){
340
+ if($results['iNotGoodTotal'][$iID] > 0){
341
+ //This has a litle bit value
342
+ if($results['iNotGoodTotal'][$iID] > 1){
343
+ $im = ' images ';
344
+ }
345
+ else{
346
+ $im = ' image ';
347
+ }
348
+
349
+ $msg[$iID]['iNotGood']['msg'] = __($results['iNotGoodTotal'][$iID] .$im. 'with wrong size', 'wp-meta-seo');
350
+ $msg[$iID]['iNotGood']['warning'] = true;
351
+
352
+ $msg[$iID]['iNotGood']['button'] = '<a href="javascript:void(0);" class=" img-resize button button-primary" data-img-name="'.$_imgs[$iID].'" data-post-id="'. $iID.'" data-opt-key="resize_image" onclick="showPostsList(this)">'.__('Resize image', 'wp-meta-seo').'<span class="spinner-light"></span></a>';
353
+
354
+ update_post_meta($iID, '_metaseo_resize_image_counter', $results['iNotGoodTotal'][$iID]);
355
+ update_post_meta($iID, '_metaseo_resize_image', $post_group);
356
+ }else{
357
+ $msg[$iID]['iNotGood']['msg'] = __('Image sizes are good!', 'wp-meta-seo');
358
+ $msg[$iID]['iNotGood']['warning'] = false;
359
+
360
+ delete_post_meta($iID, '_metaseo_resize_image_counter');
361
+ delete_post_meta($iID, '_metaseo_resize_image');
362
+ }
363
+ }
364
+ }
365
+
366
+ if(count($results['imNotGood']) > 0){
367
+ foreach($results['imNotGood'] as $iID => $post_group){
368
+ //This has a litle bit value
369
+ foreach($meta_keys as $mkey){
370
+ $text = '';
371
+ if($mkey == 'alt'){ $text = ' text'; }
372
+
373
+ if($results['imNotGoodTotal'][$iID][$mkey] > 1){
374
+ $msg[$iID]['imNotGood']['msg'][$mkey] = __($results['imNotGoodTotal'][$iID][$mkey] . ' ' . $mkey . $text . 's are missing', 'wp-meta-seo');
375
+ }
376
+ elseif($results['imNotGoodTotal'][$iID][$mkey] == 1){
377
+ $msg[$iID]['imNotGood']['msg'][$mkey] = __($results['imNotGoodTotal'][$iID][$mkey] . ' ' . $mkey . $text . ' is missing', 'wp-meta-seo');
378
+ }
379
+
380
+ }
381
+
382
+ $msg[$iID]['imNotGood']['warning'] = true;
383
+ $msg[$iID]['imNotGood']['button'] = '<a href="javascript:void(0);" class=" fix-metas button button-primary" data-img-name="'.$_imgs[$iID].'" data-post-id="'. $iID.'" data-opt-key="fix_metas" onclick="showPostsList(this)">'.__('Fix meta', 'wp-meta-seo').'<span class="spinner-light"></span></a>';
384
+
385
+
386
+ update_post_meta($iID, '_metaseo_fix_metas_counter', count($post_group));
387
+ update_post_meta($iID, '_metaseo_fix_metas', $post_group);
388
+
389
+
390
+ if( $results['imNotGoodTotal'][$iID]['alt'] == 0
391
+ && $results['imNotGoodTotal'][$iID]['title'] == 0 ){
392
+ if($results['iNotGoodTotal'][$iID] != -1){
393
+ $msg[$iID]['imNotGood']['button'] = '<a href="javascript:void(0);" class=" fix-metas button" data-img-name="'.$_imgs[$iID].'" data-post-id="'. $iID.'" data-opt-key="fix_metas" onclick="showPostsList(this)">'.__('Edit meta', 'wp-meta-seo').'<span class="spinner-light"></span></a>';
394
+ }
395
+ else{
396
+ $msg[$iID]['imNotGood']['button'] = '';
397
+ }
398
+ $msg[$iID]['imNotGood']['warning'] = false;
399
+ $msg[$iID]['imNotGood']['msg'] = '';
400
+ }
401
+
402
+ }
403
+ }
404
+
405
+ unset($results, $imgs);
406
+
407
+ return $msg;
408
+ }
409
+
410
+ /**
411
+ *
412
+ */
413
+ public static function _optimizeImages($post_id, $img_post_id, $img_exclude){
414
+ global $wpdb;
415
+ $ret = array('success' => false, 'msg' => '');
416
+ $query = "SELECT `post_content` FROM $wpdb->posts WHERE `ID`=" . $post_id;
417
+
418
+ if(!($post_content = @$wpdb->get_row($query)->post_content)){
419
+ $ret['msg'] = __('This post is not existed or deleted, please choose one another!','wp-meta-seo');
420
+
421
+ return $ret;
422
+ }
423
+
424
+ $imgs_to_resize = get_post_meta($img_post_id, '_metaseo_resize_image', true);
425
+
426
+ if(preg_match_all('/<img [^<>]+ \/>/i', $post_content, $matches)){
427
+ $img_tags = $matches[0];
428
+ $replacement = array();
429
+ foreach($matches[0] as $order => $tag){
430
+ $replacement[$order] = $tag;
431
+ //This block of code maybe changed later
432
+ if(preg_match('/(width|height)="([^\"]+)"/i', $tag, $dimension)){
433
+ if(!isset($imgs_to_resize[$post_id])) { continue; }
434
+ foreach($imgs_to_resize[$post_id]['img_after_optm'] as $key => $img){
435
+ if(!in_array($order, $img_exclude)){
436
+ if(stripos($tag, $img['src_origin']) !== false &&
437
+ (('width' == $dimension[1] && $dimension[2] == $img['width'])
438
+ || ('height' == $dimension[1] && $dimension[2] == $img['height']))){
439
+
440
+ $replacement[$order] = str_replace($img['src_origin'], $img['src'], $tag);
441
+
442
+ }
443
+ }
444
+ }
445
+ }
446
+ #}
447
+ }
448
+
449
+ //Replace all imgs sources that have new value
450
+ $post_content = str_replace($matches[0], $replacement, $post_content);
451
+ }
452
+
453
+ //Update post content with all imgs has been just optimized
454
+ $id = wp_update_post(array(
455
+ 'ID' => $post_id,
456
+ 'post_content' => $post_content
457
+ ));
458
+
459
+ if($id){
460
+ $ret['success'] = true;
461
+ $ret['msg'] = __('Well! This image is so good now.', 'wp-meta-seo');
462
+ }
463
+ else{
464
+ $ret['msg'] = __('Opps! An error occured when updating the post, please try again', 'wp-meta-seo');
465
+ }
466
+
467
+ return $ret;
468
+ }
469
+
470
+ /**
471
+ *
472
+ */
473
+ public static function IResize($src, $width, $height, $destination){
474
+ if(!list($w_origin, $h_origin) = getimagesize($src)) return "Unsupported picture type!";
475
+
476
+ if(is_readable($destination)){
477
+ return true;
478
+ }
479
+
480
+ $type = strtolower(substr(strrchr($src,"."),1));
481
+ if($type === 'jpeg') $type = 'jpg';
482
+ switch($type){
483
+ case 'bmp': $img = imagecreatefromwbmp($src); break;
484
+ case 'gif': $img = imagecreatefromgif($src); break;
485
+ case 'jpg': $img = imagecreatefromjpeg($src); break;
486
+ case 'png': $img = imagecreatefrompng($src); break;
487
+ default : return "Unsupported picture type!";
488
+ }
489
+
490
+ $new = imagecreatetruecolor($width, $height);
491
+
492
+ // preserve transparency
493
+ if($type === "gif" or $type === "png"){
494
+ imagecolortransparent($new, imagecolorallocatealpha($new, 0, 0, 0, 127));
495
+ imagealphablending($new, false);
496
+ imagesavealpha($new, true);
497
+ }
498
+
499
+ imagecopyresampled($new, $img, 0, 0, 0, 0, $width, $height, $w_origin, $h_origin);
500
+
501
+ //Output
502
+ switch($type){
503
+ case 'bmp': imagewbmp($new, $destination); break;
504
+ case 'gif': imagegif($new, $destination); break;
505
+ case 'jpg': imagejpeg($new, $destination); break;
506
+ case 'png': imagepng($new, $destination); break;
507
+ }
508
+
509
+ return true;
510
+ }
511
+
512
+ }
513
+
inc/class.metaseo-admin.php ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ //Main plugin functions here
4
+ class MetaSeo_Admin {
5
+
6
+ function __construct() {
7
+
8
+ add_action('admin_menu', array($this, 'register_menu_page'));
9
+
10
+ /** Load admin js * */
11
+ add_action('admin_enqueue_scripts', array($this, 'loadAdminScripts'));
12
+
13
+ /** Load admin css * */
14
+ add_action('admin_init', array($this, 'addAdminStylesheets'));
15
+
16
+ $this->ajaxHandle();
17
+
18
+ //register ajax update meta handler...
19
+ add_action( 'wp_ajax_updateContentMeta', array($this, 'updateContentMeta_callback') );
20
+ add_action( 'admin_init', array($this, 'stop_heartbeat') , 1 );
21
+
22
+
23
+ }
24
+
25
+ function stop_heartbeat() {
26
+ global $pagenow;
27
+ if ( 'post.php' != $pagenow && 'post-new.php' != $pagenow )
28
+ wp_deregister_script('heartbeat');
29
+ }
30
+
31
+ function updateContentMeta_callback() {
32
+ global $wpdb;
33
+ $_POST = stripslashes_deep( $_POST );
34
+ $response = new stdClass();
35
+
36
+ if( !empty( $_POST['metakey'] ) && !empty( $_POST['postid'] ) && !empty( $_POST['value'] ) );
37
+ $metakey = strtolower(trim($_POST['metakey']));
38
+ $postID = intval($_POST['postid']);
39
+ $value = trim($_POST['value']);
40
+
41
+ // if(preg_match('/[<>\/\'\"]+/', $value)){
42
+ // $response->updated = false;
43
+ // $response->msg = 'Meta content should not contains html tag or special char';
44
+ //
45
+ // echo json_encode($response);
46
+ // wp_die();
47
+ // }
48
+
49
+ $response->msg = __('Modification was saved', 'wp-meta-seo') ;
50
+ if($metakey == 'metatitle') {
51
+ if(!update_post_meta($postID, '_metaseo_metatitle', $value)) {
52
+ $response->updated = false;
53
+ $response->msg = __('Meta title was not saved', 'wp-meta-seo') ;
54
+ }
55
+ else{
56
+ $response->updated = true;
57
+ $response->msg = __('Meta title was saved', 'wp-meta-seo') ;
58
+ }
59
+ }
60
+
61
+ if($metakey =='metadesc') {
62
+ if(!update_post_meta($postID, '_metaseo_metadesc', $value)) {
63
+ $response->updated = false;
64
+ $response->msg = __('Meta description was not saved', 'wp-meta-seo') ;
65
+ }
66
+ else{
67
+ $response->updated = true;
68
+ $response->msg = __('Meta description was saved', 'wp-meta-seo') ;
69
+ }
70
+ }
71
+
72
+ echo json_encode($response);
73
+ wp_die();
74
+ }
75
+
76
+ /**
77
+ * Loads js/ajax scripts
78
+ *
79
+ */
80
+ public function loadAdminScripts($hook) {
81
+
82
+ wp_enqueue_script('jquery');
83
+
84
+ wp_enqueue_script(
85
+ 'wpmetaseoAdmin', plugins_url('js/metaseo_admin.js', dirname(__FILE__)), array('jquery'), '0.1', true
86
+ );
87
+
88
+ wp_enqueue_script('Chart', plugins_url('js/Chart.js', dirname(__FILE__)), array('jquery'), '0.1', true);
89
+ wp_enqueue_script('dashboard-chart', plugins_url('js/dashboard-chart.js', dirname(__FILE__)), array('jquery'), '0.1', true);
90
+
91
+ // in JavaScript, object properties are accessed as ajax_object.ajax_url, ajax_object.we_value
92
+ wp_localize_script( 'wpmetaseoAdmin', 'myAjax',
93
+ array( 'ajax_url' => admin_url( 'admin-ajax.php' ) ) );
94
+ }
95
+
96
+ /**
97
+ * Load additional admin stylesheets
98
+ * of jquery-ui
99
+ *
100
+ */
101
+ function addAdminStylesheets() {
102
+
103
+ wp_enqueue_style('wpmetaseoAdmin', plugins_url('css/metaseo_admin.css', dirname(__FILE__)));
104
+ wp_enqueue_style('tooltip-metaimage', plugins_url('/css/tooltip-metaimage.css',dirname(__FILE__)));
105
+ wp_enqueue_style('style', plugins_url('/css/style.css', dirname(__FILE__) ) );
106
+ wp_enqueue_style('chart', plugins_url('/css/chart.css', dirname(__FILE__) ) );
107
+
108
+ }
109
+
110
+ function register_menu_page() {
111
+
112
+ // Add main page
113
+ $admin_page = add_menu_page(__('WP Meta SEO:', 'wp-meta-seo') . ' ' . __('Dashboard', 'wp-meta-seo'), __('WP Meta SEO', 'wp-meta-seo'), 'manage_options', 'metaseo_dashboard', array(
114
+ $this,
115
+ 'load_page',
116
+ ), plugins_url('/img/icon.png', dirname(__FILE__)) );
117
+
118
+ /**
119
+ * Filter: 'metaseo_manage_options_capability' - Allow changing the capability users need to view the settings pages
120
+ *
121
+ * @api string unsigned The capability
122
+ */
123
+ $manage_options_cap = apply_filters('metaseo_manage_options_capability', 'manage_options');
124
+
125
+ // Sub menu pages
126
+ $submenu_pages = array(
127
+ array(
128
+ 'metaseo_dashboard',
129
+ '',
130
+ __('Content meta', 'wp-meta-seo'),
131
+ $manage_options_cap,
132
+ 'metaseo_content_meta',
133
+ array($this, 'load_page'),
134
+ null,
135
+ ),
136
+ array(
137
+ 'metaseo_dashboard',
138
+ '',
139
+ __('Image meta', 'wp-meta-seo'),
140
+ $manage_options_cap,
141
+ 'metaseo_image_meta',
142
+ array($this, 'load_page'),
143
+ null,
144
+ ),
145
+ );
146
+
147
+
148
+
149
+ // Allow submenu pages manipulation
150
+ $submenu_pages = apply_filters('metaseo_submenu_pages', $submenu_pages);
151
+
152
+ // Loop through submenu pages and add them
153
+ if (count($submenu_pages)) {
154
+ foreach ($submenu_pages as $submenu_page) {
155
+
156
+ // Add submenu page
157
+ $admin_page = add_submenu_page($submenu_page[0], $submenu_page[2] . ' - ' . __('WP Meta SEO:', 'wp-meta-seo'), $submenu_page[2], $submenu_page[3], $submenu_page[4], $submenu_page[5]);
158
+
159
+ // Check if we need to hook
160
+ if (isset($submenu_page[6]) && null != $submenu_page[6] && is_array($submenu_page[6]) && count($submenu_page[6]) > 0) {
161
+ foreach ($submenu_page[6] as $submenu_page_action) {
162
+ add_action('load-' . $admin_page, $submenu_page_action);
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ global $submenu;
169
+ if (isset($submenu['metaseo_dashboard']) && current_user_can($manage_options_cap)) {
170
+ $submenu['metaseo_dashboard'][0][0] = __('Dashboard', 'wp-meta-seo');
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Load the form for a WPSEO admin page
176
+ */
177
+ function load_page() {
178
+ if (isset($_GET['page'])) {
179
+ switch ($_GET['page']) {
180
+ case 'metaseo_content_meta':
181
+ require_once( WPMETASEO_PLUGIN_DIR . 'inc/pages/content-meta.php' );
182
+ break;
183
+
184
+
185
+ case 'metaseo_image_meta':
186
+ require_once( WPMETASEO_PLUGIN_DIR . 'inc/pages/image-meta.php' );
187
+ break;
188
+
189
+ case 'metaseo_image_optimize':
190
+ require_once( WPMETASEO_PLUGIN_DIR . 'inc/pages/image-optimize.php' );
191
+ break;
192
+
193
+ case 'metaseo_dashboard':
194
+ default:
195
+ require_once( WPMETASEO_PLUGIN_DIR . 'inc/pages/dashboard.php' );
196
+ break;
197
+ }
198
+ }
199
+ }
200
+
201
+ private function ajaxHandle(){
202
+ //
203
+ add_action( 'wp_ajax_scanPosts', array('MetaSeo_Image_List_Table', 'scan_posts_callback') );
204
+ add_action( 'wp_ajax_load_posts', array('MetaSeo_Image_List_Table', 'load_posts_callback') );
205
+ add_action( 'wp_ajax_optimize_imgs', array('MetaSeo_Image_List_Table', 'optimizeImages') );
206
+ add_action( 'wp_ajax_updateMeta', array('MetaSeo_Image_List_Table', 'updateMeta_callback') );
207
+ add_action( 'wp_ajax_opt_checking', array('MetaSeo_Dashboard', 'optimizationChecking') );
208
+ //
209
+ add_action( 'wp_ajax_import_meta_data', array('MetaSeo_Content_List_Table', 'importMetaData') );
210
+ add_action( 'wp_ajax_dismiss_import_meta', array('MetaSeo_Content_List_Table', 'dismissImport') );
211
+ //
212
+ add_action( 'added_post_meta' , array( 'MetaSeo_Content_List_Table', 'updateMetaSync' ), 99, 4);
213
+ add_action( 'updated_post_meta', array( 'MetaSeo_Content_List_Table', 'updateMetaSync' ), 99, 4);
214
+ add_action( 'deleted_post_meta', array( 'MetaSeo_Content_List_Table', 'deleteMetaSync' ), 99, 4);
215
+
216
+ }
217
+
218
+ }
inc/class.metaseo-content-list-table.php ADDED
@@ -0,0 +1,621 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /*
3
+ * Comments to come later
4
+ *
5
+ *
6
+ */
7
+
8
+ if (!class_exists('WP_List_Table')) {
9
+ require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
10
+ }
11
+
12
+ class MetaSeo_Content_List_Table extends WP_List_Table {
13
+ function __construct() {
14
+ parent::__construct(array(
15
+ 'singular' => 'metaseo_content',
16
+ 'plural' => 'metaseo_contents',
17
+ 'ajax' => true
18
+ ));
19
+
20
+ }
21
+
22
+ function display_tablenav($which) {
23
+ ?>
24
+ <div class="tablenav <?php echo esc_attr($which); ?>">
25
+
26
+ <?php #if($which=='top'): ?>
27
+ <input type="hidden" name="page" value="metaseo_content_meta" />
28
+ <?php #endif ?>
29
+
30
+ <input type="hidden" name="page" value="metaseo_content_meta" />
31
+ <?php if (!empty($_REQUEST['post_status'])): ?>
32
+ <input type="hidden" name="post_status" value="<?php echo esc_attr($_REQUEST['post_status']); ?>" />
33
+ <?php endif ?>
34
+
35
+ <?php $this->extra_tablenav($which); ?>
36
+
37
+ <div style="float:right;margin-left:8px;">
38
+ <input type="number" required min="1" value="<?php echo $this->_pagination_args['per_page'] ?>" maxlength="3" name="metaseo_posts_per_page" class="metaseo_imgs_per_page screen-per-page" max="999" min="1" step="1">
39
+ <input type="submit" name="btn_perpage" class="button_perpage button" id="button_perpage" value="Apply" >
40
+ </div>
41
+
42
+ <?php $this->pagination($which); ?>
43
+ <br class="clear" />
44
+ </div>
45
+
46
+ <?php
47
+ }
48
+
49
+ function get_views() {
50
+ global $wpdb;
51
+
52
+
53
+ $status_links = array();
54
+
55
+ $post_types = get_post_types(array('public' => true, 'exclude_from_search' => false));
56
+ $post_types = "'" . implode("', '", $post_types) . "'";
57
+
58
+ $states = get_post_stati(array('show_in_admin_all_list' => true));
59
+ $states['trash'] = 'trash';
60
+ $all_states = "'" . implode("', '", $states) . "'";
61
+
62
+ $total_posts = $wpdb->get_var("SELECT COUNT(*) FROM $wpdb->posts WHERE post_status IN ($all_states) AND post_type IN ($post_types)");
63
+
64
+ $class = empty($_REQUEST['post_status']) ? ' class="current"' : '';
65
+ $status_links['all'] = "<a href='admin.php?page=metaseo_content_meta'$class>" . sprintf(_nx('All <span class="count">(%s)</span>', 'All <span class="count">(%s)</span>', $total_posts, 'posts'), number_format_i18n($total_posts)) . '</a>';
66
+
67
+ foreach (get_post_stati(array('show_in_admin_all_list' => true), 'objects') as $status) {
68
+
69
+ $status_name = $status->name;
70
+
71
+ $total = $wpdb->get_var("SELECT COUNT(*) FROM $wpdb->posts WHERE post_status IN ('$status_name') AND post_type IN ($post_types)");
72
+
73
+ if ($total == 0) {
74
+ continue;
75
+ }
76
+
77
+ if (isset($_REQUEST['post_status']) && $status_name == $_REQUEST['post_status']) {
78
+ $class = ' class="current"';
79
+ } else {
80
+ $class = '';
81
+ }
82
+
83
+ $status_links[$status_name] = "<a href='admin.php?page=metaseo_content_meta&amp;post_status=$status_name'$class>" . sprintf(translate_nooped_plural($status->label_count, $total), number_format_i18n($total)) . '</a>';
84
+ }
85
+ $trashed_posts = $wpdb->get_var("SELECT COUNT(*) FROM $wpdb->posts WHERE post_status IN ('trash') AND post_type IN ($post_types)");
86
+ $class = ( isset($_REQUEST['post_status']) && 'trash' == $_REQUEST['post_status'] ) ? 'class="current"' : '';
87
+ $status_links['trash'] = "<a href='admin.php?page=metaseo_content_meta&amp;post_status=trash'$class>" . sprintf(_nx('Trash <span class="count">(%s)</span>', 'Trash <span class="count">(%s)</span>', $trashed_posts, 'posts'), number_format_i18n($trashed_posts)) . '</a>';
88
+
89
+ return $status_links;
90
+ }
91
+
92
+ function extra_tablenav($which) {
93
+
94
+ #if ('top' == $which) {
95
+ echo '<div class="alignleft actions">';
96
+ global $wpdb;
97
+
98
+ $post_types = get_post_types(array('public' => true, 'exclude_from_search' => false));
99
+ $post_types = "'" . implode("', '", $post_types) . "'";
100
+
101
+ $states = get_post_stati(array('show_in_admin_all_list' => true));
102
+ $states['trash'] = 'trash';
103
+ $all_states = "'" . implode("', '", $states) . "'";
104
+
105
+ $query = "SELECT DISTINCT post_type FROM $wpdb->posts WHERE post_status IN ($all_states) AND post_type IN ($post_types) ORDER BY 'post_type' ASC";
106
+ $post_types = $wpdb->get_results($query);
107
+
108
+ $selected = !empty($_REQUEST['post_type_filter']) ? $_REQUEST['post_type_filter'] : -1;
109
+
110
+ $options = '<option value="-1">Show All Post Types</option>';
111
+
112
+ foreach ($post_types as $post_type) {
113
+ $obj = get_post_type_object($post_type->post_type);
114
+ $options .= sprintf('<option value="%2$s" %3$s>%1$s</option>', $obj->labels->name, $post_type->post_type, selected($selected, $post_type->post_type, false));
115
+ }
116
+
117
+ if($which=='top') {
118
+ echo sprintf('<select name="post_type_filter" class="metaseo-filter">%1$s</select>', $options);
119
+ submit_button(__('Filter'), 'button', 'do_filter', false, array('id' => 'post-query-submit'));
120
+ }else{
121
+ echo sprintf('<select name="post_type_filter" class="metaseo-filter">%1$s</select>', $options);
122
+ submit_button(__('Filter'), 'button', 'do_filter', false, array('id' => 'post-query-submit'));
123
+ }
124
+
125
+ echo "</div>";
126
+ #echo "</form>";
127
+ #}
128
+ }
129
+
130
+ function get_columns() {
131
+ return $columns = array(
132
+ 'col_id' => __('', 'wp-meta-seo'),
133
+ 'col_title' => __('Title', 'wp-meta-seo'),
134
+ 'col_meta_title' => __('Meta Title', 'wp-meta-seo'),
135
+ 'col_meta_desc' => __('Meta Description', 'wp-meta-seo')
136
+ );
137
+ }
138
+
139
+ function get_sortable_columns() {
140
+ return $sortable = array(
141
+ 'col_title' => array('post_title', true),
142
+ );
143
+ }
144
+
145
+ function prepare_items() {
146
+ global $wpdb, $_wp_column_headers;
147
+ //$GLOBALS['wp_filter']["manage_{$GLOBALS['screen']->id}_screen_columns"];
148
+
149
+ $screen = get_current_screen();
150
+
151
+ $where = array();
152
+ $post_type = isset($_REQUEST['post_type_filter'])? $_REQUEST['post_type_filter'] : "";
153
+ if($post_type=="-1") {
154
+ $post_type="";
155
+ }
156
+
157
+ $post_types = get_post_types( array('public' => true, 'exclude_from_search' => false) ) ;
158
+ if ( !empty( $post_type ) && !in_array( $post_type,$post_types ) )
159
+ $post_type = '\'post\'';
160
+ else if(empty ($post_type)) {
161
+ $post_type = "'" . implode("', '", $post_types) . "'";
162
+ }else {
163
+ $post_type = "'" . $post_type . "'";
164
+ }
165
+ $where[] = "post_type IN ($post_type)";
166
+
167
+ $states = get_post_stati(array('show_in_admin_all_list' => true));
168
+ $states['trash'] = 'trash';
169
+ $all_states = "'" . implode("', '", $states) . "'";
170
+
171
+ if (empty($_REQUEST['post_status'])) {
172
+ $where[] = "post_status IN ($all_states)";
173
+ } else {
174
+ $requested_state = $_REQUEST['post_status'];
175
+ if (in_array($requested_state, $states)) {
176
+ $where[] = "post_status IN ('$requested_state')";
177
+ } else {
178
+ $where[] = "post_status IN ($all_states)";
179
+ }
180
+ }
181
+
182
+ //Order By block
183
+ $orderby = !empty($_GET["orderby"]) ? ($_GET["orderby"]) : 'post_title';
184
+ $order = !empty($_GET["order"]) ? ($_GET["order"]) : 'asc';
185
+
186
+ $sortable = $this->get_sortable_columns();
187
+ if(in_array($orderby, $sortable)) {
188
+ $orderStr = $orderby;
189
+ }else {
190
+ $orderStr = 'post_title';
191
+ }
192
+
193
+ if($order=="asc") {
194
+ $orderStr .= " ASC";
195
+ }else {
196
+ $orderStr .= " DESC";
197
+ }
198
+
199
+ if (!empty($orderby) & !empty($order)) {
200
+ $orderStr =' ORDER BY ' . $orderStr;
201
+ }
202
+
203
+ $query = "SELECT ID, post_title, post_name, post_type, post_status , mt.meta_value AS metatitle, md.meta_value AS metadesc "
204
+ . " FROM $wpdb->posts "
205
+ . " LEFT JOIN (SELECT * FROM $wpdb->postmeta WHERE meta_key = '_metaseo_metatitle') mt ON mt.post_id = $wpdb->posts.ID "
206
+ . " LEFT JOIN (SELECT * FROM $wpdb->postmeta WHERE meta_key = '_metaseo_metadesc') md ON md.post_id = $wpdb->posts.ID "
207
+ . " WHERE ". implode(' AND ', $where) . $orderStr;
208
+
209
+ $total_items = $wpdb->query($query);
210
+
211
+ if(!empty($_REQUEST['metaseo_posts_per_page'])){
212
+ $_per_page = intval($_REQUEST['metaseo_posts_per_page']);
213
+ }
214
+ else {
215
+ $_per_page = 0;
216
+ }
217
+ $per_page = get_user_option('metaseo_posts_per_page');
218
+ if( $per_page !== false) {
219
+ if($_per_page && $_per_page !== $per_page ){
220
+ $per_page = $_per_page;
221
+ update_user_option(get_current_user_id(), 'metaseo_posts_per_page', $per_page);
222
+ }
223
+ }
224
+ else{
225
+ if($_per_page > 0) {
226
+ $per_page = $_per_page;
227
+ }
228
+ else { $per_page = 10; }
229
+ add_user_meta(get_current_user_id(), 'metaseo_posts_per_page', $per_page);
230
+ }
231
+
232
+ $paged = !empty($_GET["paged"]) ? $_GET["paged"] : '';
233
+ if (empty($paged) || !is_numeric($paged) || $paged <= 0) {
234
+ $paged = 1;
235
+ }
236
+
237
+ $total_pages = ceil($total_items / $per_page);
238
+
239
+ if (!empty($paged) && !empty($per_page)) {
240
+ $offset = ($paged - 1) * $per_page;
241
+ $query .= ' LIMIT ' . (int) $offset . ',' . (int) $per_page;
242
+ }
243
+
244
+ $this->set_pagination_args(array(
245
+ 'total_items' => $total_items,
246
+ 'total_pages' => $total_pages,
247
+ 'per_page' => $per_page
248
+ ));
249
+
250
+ $columns = $this->get_columns();
251
+ $hidden = array();
252
+ $sortable = $this->get_sortable_columns();
253
+ $this->_column_headers = array($columns, $hidden, $sortable);
254
+
255
+ $this->items = $wpdb->get_results($query);
256
+ }
257
+
258
+ function display_rows() {
259
+
260
+ $records = $this->items;
261
+ $i = 0;
262
+ $alternate = "";
263
+ $url = preg_replace('/(http|https):\/\/[w]*[.]?/', '', network_site_url('/'));
264
+
265
+ list( $columns, $hidden ) = $this->get_column_info();
266
+
267
+ if (!empty($records)) {
268
+ foreach ($records as $rec) {
269
+ $alternate = 'alternate' == $alternate ? '' : 'alternate';
270
+ $i++;
271
+ $classes = $alternate;
272
+ $rec->link = $url;
273
+
274
+ echo '<tr id="record_' . $rec->ID . '" class="' . $classes . '" >';
275
+
276
+ foreach ($columns as $column_name => $column_display_name) {
277
+
278
+ $class = sprintf('class="%1$s column-%1$s"', $column_name);
279
+ $style = "";
280
+
281
+ if (in_array($column_name, $hidden)) {
282
+ $style = ' style="display:none;"';
283
+ }
284
+
285
+ $attributes = $class . $style;
286
+
287
+ switch ($column_name) {
288
+ case 'col_id':
289
+ echo '<td class="col_id" >';
290
+ echo $i;
291
+ echo '</td>';
292
+
293
+ break;
294
+
295
+ case 'col_title':
296
+ echo sprintf('<td %2$s><div class="action-wrapper"><strong id="post-title-'.$rec->ID.'">%1$s</strong>', stripslashes($rec->post_title), $attributes);
297
+
298
+ $post_type_object = get_post_type_object($rec->post_type);
299
+ $can_edit_post = current_user_can($post_type_object->cap->edit_post, $rec->ID);
300
+
301
+ $actions = array();
302
+
303
+ if ($can_edit_post && 'trash' != $rec->post_status) {
304
+ $actions['edit'] = '<a href="' . get_edit_post_link($rec->ID, true) . '" title="' . esc_attr(__('Edit this item')) . '">' . __('Edit') . '</a>';
305
+ }
306
+
307
+ if ($post_type_object->public) {
308
+ if (in_array($rec->post_status, array('pending', 'draft', 'future'))) {
309
+ if ($can_edit_post)
310
+ $actions['view'] = '<a href="' . esc_url(add_query_arg('preview', 'true', get_permalink($rec->ID))) . '" title="' . esc_attr(sprintf(__('Preview &#8220;%s&#8221;'), $rec->post_title)) . '" rel="permalink">' . __('Preview') . '</a>';
311
+ } elseif ('trash' != $rec->post_status) {
312
+ $actions['view'] = '<a target="_blank" href="' . get_permalink($rec->ID) . '" title="' . esc_attr(sprintf(__('View &#8220;%s&#8221;'), $rec->post_title)) . '" rel="permalink">' . __('View') . '</a>';
313
+ }
314
+ }
315
+
316
+ echo $this->row_actions($actions);
317
+ echo '</div><div class="snippet-wrapper">';
318
+ $preview = __(" This is a rendering of what this post might look like in Google's search results.", 'wp-meta-seo');
319
+ $info = sprintf('<a class="info-content"><img src=' . WPMETASEO_PLUGIN_URL . 'img/info.png href="#">'
320
+ . '<p class="tooltip-metacontent">'
321
+ .$preview
322
+ .'</p></a>');
323
+
324
+ echo '<div><strong>' . __('Snippet Preview', 'wp-meta-seo') . '</strong> ' . $info. '</div>';
325
+
326
+ echo '<div class="snippet">
327
+ <a id="snippet_title'.$rec->ID . '" class="snippet_metatitle">'.(!empty($rec->metatitle) ? $rec->metatitle : $rec->post_title).'</a>';
328
+
329
+ echo '
330
+ <span class="snippet_metalink" id="snippet_metalink_'.$rec->ID.'">'.$rec->link.'</span>';
331
+
332
+ echo '
333
+ <p id="snippet_desc'.$rec->ID . '" class="snippet_metades">'.$rec->metadesc.'</p>
334
+ </div>';
335
+ echo '
336
+ <span id="savedInfo'.$rec->ID.'" style="position: relative; display: block;float:right" class="saved-info metaseo-msg-success"><span style="position:absolute; float:right" class="meta-update"></span></span>';
337
+ //echo '<span id="savedInfo'.$rec->ID.'" class="savedInfo" style="display:none;"></span>';
338
+ echo '</div>';
339
+ // echo sprintf('<div %2$s>%1$s</div>', $info, $attributes);
340
+
341
+
342
+
343
+ break;
344
+ case 'col_page_slug':
345
+ $permalink = get_permalink($rec->ID);
346
+ $display_slug = str_replace(get_bloginfo('url'), '', $permalink);
347
+ echo sprintf('<td %2$s><a href="%3$s" target="_blank">%1$s</a></td>', stripslashes($display_slug), $attributes, $permalink);
348
+ break;
349
+
350
+ case 'col_meta_title':
351
+ $input = sprintf('</br><textarea class="large-text metaseo-metatitle" rows="3" id="%1$s" name="%2$s" autocomplete="off">%3$s</textarea>', 'metaseo-metatitle-' . $rec->ID, 'metatitle['. $rec->ID.']', ( ($rec->metatitle ) ? $rec->metatitle : ''));
352
+ $input .= sprintf('<div class="title-len" id="%1$s"></div>', 'metaseo-metatitle-len' . $rec->ID);
353
+ echo sprintf('<td %2$s>%1$s</td>', $input, $attributes);
354
+ break;
355
+
356
+ case 'col_meta_desc':
357
+ $input = sprintf('</br><textarea class="large-text metaseo-metadesc" rows="3" id="%1$s" name="%2$s" autocomplete="off">%3$s</textarea>', 'metaseo-metadesc-' . $rec->ID, ' metades['. $rec->ID.']', (($rec->metadesc ) ? $rec->metadesc : ''));
358
+ $input .= sprintf('<div class="desc-len" id="%1$s"></div>', 'metaseo-metadesc-len' . $rec->ID);
359
+ echo sprintf('<td %2$s>%1$s</td>', $input, $attributes);
360
+ break;
361
+ }
362
+ }
363
+
364
+ echo '</tr>';
365
+ }
366
+ }
367
+ }
368
+
369
+ protected function get_bulk_actions() {
370
+ $actions = array();
371
+ $actions = array(
372
+ 'update' => 'Update',
373
+ );
374
+
375
+ return $actions;
376
+ }
377
+
378
+ function process_action() {
379
+ $current_url = set_url_scheme('http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
380
+ $redirect = false;
381
+
382
+ if (isset($_POST['do_filter']) and $_POST['do_filter'] === 'Filter') {
383
+ $current_url = add_query_arg(array( "post_type_filter" => $_POST['post_type_filter']), $current_url);
384
+ $redirect = true;
385
+ }
386
+
387
+ if(!empty($_POST['paged'])){
388
+ $current_url = add_query_arg(array( "paged" => intval($_POST['paged'])), $current_url);
389
+ $redirect = true;
390
+ }
391
+
392
+ if(!empty($_POST['metaseo_posts_per_page'])){
393
+ $current_url = add_query_arg(array( "metaseo_posts_per_page" => intval($_POST['metaseo_posts_per_page'])), $current_url);
394
+ $redirect = true;
395
+ }
396
+
397
+ if($redirect === true){
398
+ wp_redirect($current_url);
399
+ ob_end_flush();
400
+ exit();
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Get all posts that is public and contain images with a string seperated by comma
406
+ */
407
+ public static function get_post_types(){
408
+ global $wpdb;
409
+ $post_types = get_post_types( array('public' => true, 'exclude_from_search' => false) ) ;
410
+ if(!empty($post_types)){
411
+ $post_types = "'" . implode("', '", $post_types) . "'";
412
+ }
413
+
414
+ return $post_types;
415
+ }
416
+
417
+ public static function importMetaData(){
418
+ global $wpdb;
419
+ $meta_metaseo_keys = array('_metaseo_metatitle', '_metaseo_metadesc');
420
+ $meta_other_keys = array(
421
+ '_aio_' => array('_aioseop_title', '_aioseop_description'),
422
+ '_yoast_' => array('_yoast_wpseo_title', '_yoast_wpseo_metadesc')
423
+ );
424
+
425
+ if(!empty($_POST['plugin']) and in_array(strtolower(trim($_POST['plugin'])), array('_aio_', '_yoast_'))){
426
+ $plugin = strtolower(trim($_POST['plugin']));
427
+ $metakeys = '';
428
+ foreach($meta_metaseo_keys as $k => $mkey){
429
+ $metakeys .= ' OR `meta_key` = \''. $mkey . '\' OR `meta_key` = \'' . $meta_other_keys[$plugin][$k] . '\'';
430
+ }
431
+
432
+ $metakeys = ltrim($metakeys, ' OR ');
433
+ $query = "SELECT `post_id` as pID, `meta_key`, `meta_value`
434
+ FROM $wpdb->postmeta
435
+ WHERE $metakeys
436
+ ORDER BY `meta_key`";
437
+ $posts_metas = $wpdb->get_results($query);
438
+
439
+ if(is_array($posts_metas) && count($posts_metas) > 0){
440
+ foreach($posts_metas as $postmeta){
441
+ $_posts_metas[$postmeta->pID][$postmeta->meta_key] = $postmeta->meta_value;
442
+ }
443
+ unset($posts_metas);
444
+ foreach($_posts_metas as $pID => $pmeta){
445
+ foreach($meta_metaseo_keys as $k => $mkey){
446
+ $mvalue = $pmeta[$mkey];
447
+ $msynckey = $meta_other_keys[$plugin][$k];
448
+ $msyncvalue = $pmeta[$msynckey];
449
+
450
+ if( is_null($mvalue ) || ( $mvalue == '' && $msynckey != '' ) ){
451
+ update_post_meta( $pID, $mkey, $msyncvalue );
452
+ }
453
+ elseif( is_null($msyncvalue ) || ( $msyncvalue == '' && $mvalue != '' ) ){
454
+ update_post_meta( $pID, $msynckey, $mvalue );
455
+ }
456
+ elseif($mvalue != '' && $msyncvalue != ''){
457
+ update_post_meta( $pID, $mkey, $msyncvalue );
458
+ }
459
+ }
460
+
461
+ }
462
+
463
+ unset($posts_metas);
464
+ }
465
+
466
+
467
+ $ret['success'] = true;
468
+
469
+ update_option('_aio_import_notice_flag', 1);
470
+ update_option('_yoast_import_notice_flag', 1);
471
+ update_option('plugin_to_sync_with', $plugin);
472
+ }else{
473
+ $ret['success'] = false;
474
+ }
475
+
476
+ echo json_encode($ret);
477
+ wp_die();
478
+ }
479
+
480
+ public static function dismissImport(){
481
+ if(!empty($_POST['plugin']) and in_array(strtolower(trim($_POST['plugin'])), array('_aio_', '_yoast_'))){
482
+ $plugin = strtolower(trim($_POST['plugin']));
483
+
484
+ update_option($plugin.'import_notice_flag', 1);
485
+ $ret['success'] = true;
486
+ }else{
487
+ $ret['success'] = false;
488
+ }
489
+
490
+ echo json_encode($ret);
491
+ wp_die();
492
+ }
493
+
494
+ /**
495
+ *
496
+ */
497
+ public static function updateMetaSync($meta_id, $object_id, $meta_key, $meta_value){
498
+ if(!self::is_updateSync($meta_key)){
499
+ return null;
500
+ }
501
+
502
+ if(self::_updateMetaSync('update', $object_id, $meta_key, $meta_value)){
503
+ return true;
504
+ }
505
+
506
+ return null;
507
+ }
508
+
509
+ /**
510
+ *
511
+ */
512
+ public static function deleteMetaSync($meta_ids, $object_id, $meta_key, $meta_value){
513
+
514
+ if(!self::is_updateSync($meta_key)){
515
+ return null;
516
+ }
517
+
518
+ if(self::_updateMetaSync('delete', $object_id, $meta_key, $meta_value)){
519
+ return true;
520
+ }
521
+
522
+ return null;
523
+ }
524
+
525
+ /**
526
+ *
527
+ */
528
+ private static function _updateMetaSync($type = '', $object_id, $meta_key, $meta_value){
529
+ if( ! ( $sync = get_option('plugin_to_sync_with') ) or !in_array( $sync, array('_aio_', '_yoast_' ) ) ){
530
+ return false;
531
+ }
532
+
533
+ $metakeys = array(
534
+ '_metaseo_' => array('_metaseo_metatitle' , '_metaseo_metadesc'),
535
+ '_aio_' => array('_aioseop_title', '_aioseop_description'),
536
+ '_yoast_' => array('_yoast_wpseo_title', '_yoast_wpseo_metadesc')
537
+ );
538
+
539
+ $_metakeys = array();
540
+ $_metakeys['_metaseo_'] = $metakeys['_metaseo_'];
541
+ $_metakeys[$sync] = $metakeys[$sync];
542
+ unset($metakeys);
543
+
544
+ foreach($_metakeys as $identify => $mkeys){
545
+ foreach($mkeys as $k => $mkey){
546
+ if($meta_key === $mkey){
547
+ if($identify === '_metaseo_' ){
548
+ $mkeysync = $_metakeys[$sync][$k];
549
+ }
550
+ else{
551
+ $mkeysync = $_metakeys['_metaseo_'][$k];
552
+ }
553
+
554
+ if($type == 'update'){
555
+ update_post_meta($object_id, $mkeysync, $meta_value);
556
+ return true;
557
+ }
558
+
559
+ if($type == 'delete'){
560
+ delete_post_meta($object_id, $mkeysync);
561
+ return true;
562
+ }
563
+
564
+ }
565
+ }
566
+
567
+ }
568
+
569
+ return false;
570
+ }
571
+
572
+ /**
573
+ *
574
+ */
575
+ public static function updateMetaSyncAll($meta_id, $object_id, $meta_key, $meta_value){
576
+ if(!self::is_updateSync($meta_key)){
577
+ return null;
578
+ }
579
+ //These info may be got from database in later version
580
+ $mseo = 'wp-meta-seo/wp-meta-seo.php';
581
+ $yoast = 'wordpress-seo/wp-seo.php';
582
+ $aio = 'all-in-one-seo-pack/all_in_one_seo_pack.php';
583
+
584
+ $metakeys = array(
585
+ 'mtitle' => array(
586
+ $mseo => '_metaseo_metatitle',
587
+ $yoast => '_yoast_wpseo_title',
588
+ $aio => '_aioseop_title' ),
589
+ 'mdesciption' => array(
590
+ $mseo => '_metaseo_metadesc',
591
+ $yoast => '_yoast_wpseo_metadesc',
592
+ $aio => '_aioseop_description' )
593
+ );
594
+
595
+ //Update post meta
596
+ foreach($metakeys as $metakey){
597
+ if(in_array($meta_key, $metakey)){
598
+ foreach($metakey as $plg => $mkey){
599
+ if($mkey !== $meta_key){
600
+ if($plg === $mseo || is_plugin_active($plg)){
601
+ update_post_meta($object_id, $mkey, $meta_value);
602
+ }
603
+ }
604
+ }
605
+ }
606
+ }
607
+
608
+ return null;
609
+ }
610
+
611
+ public static function is_updateSync($meta_key){
612
+ $mkey_prefix = array('_metaseo_', '_yoast_', '_aio');
613
+ foreach($mkey_prefix as $prefix){
614
+ if(strpos($meta_key, $prefix) === 0){
615
+ return true;
616
+ }
617
+ }
618
+
619
+ return false;
620
+ }
621
+ }
inc/class.metaseo-dashboard.php ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /*
4
+ * Comments to come later
5
+ *
6
+ *
7
+ */
8
+
9
+ class MetaSeo_Dashboard {
10
+
11
+ public static function optimizationChecking() {
12
+ global $wpdb;
13
+ $imgs = 0;
14
+ #$imgs_metas = 0;
15
+ $imgs_metas = array('alt' => 0, 'title' => 0);
16
+ $imgs_are_good = 0;
17
+ $imgs_metas_are_good = array();
18
+ $meta_keys = array('alt', 'title');
19
+ $response = array(
20
+ 'imgs_statis' => array(0, 0),
21
+ 'imgs_metas_statis' => array(0, 0),
22
+ 'meta_title_statis' => array(0, 0),
23
+ 'meta_desc_statis' => array(0, 0),
24
+ 'metacontent' => array(0, 0),
25
+ );
26
+ foreach ($meta_keys as $meta_key) {
27
+ $imgs_metas_are_good[$meta_key] = 0;
28
+ $imgs_metas_are_not_good[$meta_key] = 0;
29
+ }
30
+
31
+ $post_types = MetaSeo_Content_List_Table::get_post_types();
32
+ $query = "SELECT `ID`, `post_title`, `post_content`, `post_type`, `post_date`
33
+ FROM $wpdb->posts
34
+ WHERE `post_type` IN ($post_types)
35
+ AND `post_content` <> ''
36
+ AND `post_content` LIKE '%<img%>%'
37
+ ORDER BY ID";
38
+
39
+ $posts = $wpdb->get_results($query);
40
+ if (count($posts) > 0) {
41
+ $doc = new DOMDocument();
42
+ $upload_dir = wp_upload_dir();
43
+
44
+ foreach ($posts as $post) {
45
+ $dom = $doc->loadHTML($post->post_content);
46
+ $tags = $doc->getElementsByTagName('img');
47
+ foreach ($tags as $tag) {
48
+ $img_src = $tag->getAttribute('src');
49
+
50
+ if(!preg_match('/\.(jpg|png|gif)$/i', $img_src, $matches)){
51
+ continue;
52
+ }
53
+
54
+ $img_path = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $img_src);
55
+ if(!file_exists($img_path)){
56
+ continue;
57
+ }
58
+
59
+ $width = $tag->getAttribute('width');
60
+ $height = $tag->getAttribute('height');
61
+ if (list($real_width, $real_height) = @getimagesize($img_path)) {
62
+ $ratio_origin = $real_width / $real_height;
63
+ //Check if img tag is missing with/height attribute value or not
64
+ if (!$width && !$height) {
65
+ $width = $real_width;
66
+ $height = $real_height;
67
+ } elseif ($width && !$height) {
68
+ $height = $width * (1 / $ratio_origin);
69
+ } elseif ($height && !$width) {
70
+ $width = $height * ($ratio_origin);
71
+ }
72
+
73
+ if ($real_width <= $width && $real_height <= $height) {
74
+ $imgs_are_good++;
75
+ }
76
+
77
+ foreach ($meta_keys as $meta_key) {
78
+
79
+ if (trim($tag->getAttribute($meta_key))) {
80
+ $imgs_metas_are_good[$meta_key] ++;
81
+ }
82
+
83
+ }
84
+ }
85
+
86
+ $imgs++;
87
+ }
88
+ }
89
+
90
+ //Report analytic of images optimization
91
+ $response['imgs_statis'][0] = $imgs_are_good;
92
+ $response['imgs_statis'][1] = $imgs;
93
+ $response['imgs_metas_statis'][0] = ceil(($imgs_metas_are_good['alt'] + $imgs_metas_are_good['title'])/2 ) ;
94
+ $response['imgs_metas_statis'][1] = $imgs;
95
+ }
96
+
97
+ //Get number of post/page and number of images inserted into them
98
+ $posts_counter = wp_count_posts('post');
99
+ $pages_counter = wp_count_posts('page');
100
+ $posts_pages_total = $posts_counter->publish + $pages_counter->publish;
101
+ $response['meta_title_statis'][1] = $posts_pages_total;
102
+ $response['meta_desc_statis'][1] = $posts_pages_total;
103
+
104
+ $query = "SELECT `meta_key`, count( `meta_value` ) as total
105
+ FROM $wpdb->postmeta
106
+ WHERE `meta_key` = '_metaseo_metatitle' AND `meta_value` <> ''
107
+ UNION (
108
+ SELECT `meta_key`, count( `meta_value` ) as total
109
+ FROM $wpdb->postmeta
110
+ WHERE `meta_key` = '_metaseo_metadesc' AND `meta_value` <> ''
111
+ )";
112
+
113
+ $alias_names = array('_metaseo_metatitle' => 'meta_title_statis', '_metaseo_metadesc' => 'meta_desc_statis');
114
+
115
+ $results = $wpdb->get_results($query);
116
+ if (count($results) > 0) {
117
+ $count_tt_desc = 0;
118
+ foreach ($results as $result) {
119
+ if ($result->meta_key === NULL) {
120
+ continue;
121
+ }
122
+ //Report analytic of content meta
123
+ $count_tt_desc +=(int) $result->total;
124
+ $response[$alias_names[$result->meta_key]][0] = (int) $result->total;
125
+ $response[$alias_names[$result->meta_key]][1] = $posts_pages_total;
126
+ }
127
+ $response['metacontent'][0] = ceil($count_tt_desc/2);
128
+ $response['metacontent'][1] = $posts_pages_total ;
129
+ }
130
+
131
+ echo json_encode($response);
132
+ wp_die();
133
+ }
134
+
135
+ public function displayRank($url){
136
+ $rank = $this->getRank($url);
137
+ if($rank !== ''){
138
+ echo $rank;
139
+ }
140
+ else{
141
+ echo __('We can\'t get rank of this site from Alexa.com!', 'wp-meta-seo');
142
+ }
143
+ }
144
+
145
+ public function getRank($url){
146
+ if(!function_exists('curl_version')){
147
+ if(!$content = @file_get_contents($url)){
148
+ return '';
149
+ }
150
+ }
151
+ else{
152
+ if(!is_array($url)){ $url = array($url); }
153
+ $contents = $this->get_contents($url);
154
+ $content = $contents[0];
155
+ }
156
+
157
+ $doc = new DOMDocument();
158
+ @$doc->loadHTML($content);
159
+ $doc->preserveWhiteSpace = false;
160
+
161
+ $finder = new DOMXPath($doc);
162
+ $classname = 'note-no-data';
163
+ $nodes = $finder->query("//section[contains(@class, '$classname')]");
164
+ if($nodes->length < 1) {
165
+ $classname = 'rank-row';
166
+ $nodes = $finder->query("//div[contains(@class, '$classname')]");
167
+ }
168
+
169
+ $tmp_dom = new DOMDocument();
170
+ foreach($nodes as $key => $node){
171
+ $tmp_dom->appendChild($tmp_dom->importNode($node,true));
172
+ }
173
+
174
+ $html = trim($tmp_dom->saveHTML());
175
+ $html = str_replace('We don\'t have', 'Alexa doesn\'t has', $html);
176
+ $html = str_replace('Get Certified', '', $html);
177
+ $html = str_replace('"/topsites/countries', '"http://www.alexa.com/topsites/countries', $html);
178
+ return $html;
179
+ }
180
+
181
+ public function get_contents($urls){
182
+ $mh = curl_multi_init();
183
+ $curl_array = array();
184
+ $useragent = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36';
185
+ foreach($urls as $i => $url)
186
+ {
187
+ $curl_array[$i] = curl_init($url);
188
+ curl_setopt($curl_array[$i], CURLOPT_URL, $url);
189
+ curl_setopt($curl_array[$i], CURLOPT_USERAGENT, $useragent); // set user agent
190
+ curl_setopt($curl_array[$i], CURLOPT_RETURNTRANSFER, TRUE);
191
+ curl_setopt($curl_array[$i], CURLOPT_FOLLOWLOCATION, TRUE);
192
+ curl_setopt($curl_array[$i], CURLOPT_CONNECTTIMEOUT, 5);
193
+ curl_setopt($curl_array[$i], CURLOPT_ENCODING ,"UTF-8");
194
+ curl_multi_add_handle($mh, $curl_array[$i]);
195
+
196
+ }
197
+
198
+ $running = NULL;
199
+ do {
200
+ usleep(10000);
201
+ curl_multi_exec($mh,$running);
202
+ } while($running > 0);
203
+
204
+ $contents = array();
205
+ foreach($urls as $i => $url)
206
+ {
207
+ $content = curl_multi_getcontent($curl_array[$i]);
208
+ $contents[$i] = $content;
209
+ }
210
+
211
+ foreach($urls as $i => $url){
212
+ curl_multi_remove_handle($mh, $curl_array[$i]);
213
+ }
214
+ curl_multi_close($mh);
215
+ return $contents;
216
+ }
217
+
218
+ }
inc/class.metaseo-image-list-table.php ADDED
@@ -0,0 +1,1236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /*
3
+ * Comments to come later
4
+ *
5
+ *
6
+ */
7
+
8
+ if (!class_exists('WP_List_Table')) {
9
+ require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
10
+ }
11
+
12
+ if ( !class_exists( 'ImageHelper' ) ) {
13
+ require_once( 'class.image-helper.php' );
14
+ }
15
+
16
+ class MetaSeo_Image_List_Table extends WP_List_Table {
17
+
18
+ function __construct() {
19
+ parent::__construct(array(
20
+ 'singular' => 'metaseo_image',
21
+ 'plural' => 'metaseo_images',
22
+ 'ajax' => true
23
+ ));
24
+ }
25
+
26
+ function display_tablenav($which) {
27
+ ?>
28
+ <div class="tablenav <?php echo esc_attr($which); ?>">
29
+
30
+ <?php if($which=='top'): ?>
31
+ <input type="hidden" name="page" value="metaseo_image_meta" />
32
+ <div class="alignleft actions bulkactions">
33
+ <?php $this->months_fillter('attachment','sldate','filter_date_action'); ?>
34
+ </div>
35
+ <?php elseif($which=='bottom'):?>
36
+ <input type="hidden" name="page" value="metaseo_image_meta" />
37
+ <div class="alignleft actions bulkactions">
38
+ <?php $this->months_fillter('attachment','sldate1','filter_date_action'); ?>
39
+ </div>
40
+ <?php endif ?>
41
+
42
+ <input type="hidden" name="page" value="metaseo_image_meta" />
43
+ <?php if (!empty($_REQUEST['post_status'])): ?>
44
+ <input type="hidden" name="post_status" value="<?php echo esc_attr($_REQUEST['post_status']); ?>" />
45
+ <?php endif ?>
46
+
47
+ <?php //$this->extra_tablenav($which); ?>
48
+
49
+ <div style="float:right;margin-left:8px;">
50
+ <input type="number" required min="1" value="<?php echo $this->_pagination_args['per_page'] ?>" maxlength="3" name="metaseo_imgs_per_page" class="metaseo_imgs_per_page screen-per-page" max="999" min="1" step="1">
51
+ <input type="submit" name="btn_perpage" class="button_perpage button" id="button_perpage" value="Apply" >
52
+ </div>
53
+
54
+ <?php $this->pagination($which); ?>
55
+ <br class="clear" />
56
+ </div>
57
+
58
+ <?php
59
+ }
60
+
61
+ function get_views() {
62
+ global $wpdb;
63
+
64
+
65
+ $status_links = array();
66
+
67
+ $post_types = get_post_types(array('public' => true, 'exclude_from_search' => false));
68
+ $post_types = "'" . implode("', '", $post_types) . "'";
69
+
70
+ $states = get_post_stati(array('show_in_admin_all_list' => true));
71
+ $states['trash'] = 'trash';
72
+ $all_states = "'" . implode("', '", $states) . "'";
73
+
74
+ $total_posts = $wpdb->get_var("SELECT COUNT(*) FROM $wpdb->posts WHERE post_status IN ($all_states) AND post_type IN ($post_types)");
75
+
76
+ $class = empty($_REQUEST['post_status']) ? ' class="current"' : '';
77
+ $status_links['all'] = "<a href='admin.php?page=metaseo_image_meta'$class>" . sprintf(_nx('All <span class="count">(%s)</span>', 'All <span class="count">(%s)</span>', $total_posts, 'posts'), number_format_i18n($total_posts)) . '</a>';
78
+
79
+ foreach (get_post_stati(array('show_in_admin_all_list' => true), 'objects') as $status) {
80
+
81
+ $status_name = $status->name;
82
+
83
+ $total = $wpdb->get_var("SELECT COUNT(*) FROM $wpdb->posts WHERE post_status IN ('$status_name') AND post_type IN ($post_types)");
84
+
85
+ if ($total == 0) {
86
+ continue;
87
+ }
88
+
89
+ if (isset($_REQUEST['post_status']) && $status_name == $_REQUEST['post_status']) {
90
+ $class = ' class="current"';
91
+ } else {
92
+ $class = '';
93
+ }
94
+
95
+ $status_links[$status_name] = "<a href='admin.php?page=metaseo_image_meta&amp;post_status=$status_name'$class>" . sprintf(translate_nooped_plural($status->label_count, $total), number_format_i18n($total)) . '</a>';
96
+ }
97
+ $trashed_posts = $wpdb->get_var("SELECT COUNT(*) FROM $wpdb->posts WHERE post_status IN ('trash') AND post_type IN ($post_types)");
98
+ $class = ( isset($_REQUEST['post_status']) && 'trash' == $_REQUEST['post_status'] ) ? 'class="current"' : '';
99
+ $status_links['trash'] = "<a href='admin.php?page=metaseo_image_meta&amp;post_status=trash'$class>" . sprintf(_nx('Trash <span class="count">(%s)</span>', 'Trash <span class="count">(%s)</span>', $trashed_posts, 'posts'), number_format_i18n($trashed_posts)) . '</a>';
100
+
101
+ return $status_links;
102
+ }
103
+
104
+ function extra_tablenav($which) {
105
+
106
+ #if ('top' == $which) {
107
+ echo '<div class="alignleft actions">';
108
+ global $wpdb;
109
+
110
+ $post_types = get_post_types(array('public' => true, 'exclude_from_search' => false));
111
+ $post_types = "'" . implode("', '", $post_types) . "'";
112
+
113
+ $states = get_post_stati(array('show_in_admin_all_list' => true));
114
+ $states['trash'] = 'trash';
115
+ $all_states = "'" . implode("', '", $states) . "'";
116
+
117
+ $query = "SELECT DISTINCT post_type FROM $wpdb->posts WHERE post_status IN ($all_states) AND post_type IN ($post_types) ORDER BY 'post_type' ASC";
118
+ $post_types = $wpdb->get_results($query);
119
+
120
+ $selected = !empty($_REQUEST['post_type_filter']) ? $_REQUEST['post_type_filter'] : -1;
121
+
122
+ $options = '<option value="-1">Show All Post Types</option>';
123
+
124
+ foreach ($post_types as $post_type) {
125
+ $obj = get_post_type_object($post_type->post_type);
126
+ $options .= sprintf('<option value="%2$s" %3$s>%1$s</option>', $obj->labels->name, $post_type->post_type, selected($selected, $post_type->post_type, false));
127
+ }
128
+
129
+ echo "</div>";
130
+ #echo "</form>";
131
+ #}
132
+ }
133
+
134
+ function get_columns() {
135
+ return $columns = array(
136
+ 'col_id' => __('ID', 'wp-meta-seo'),
137
+ 'col_image' => __('Image', 'wp-meta-seo'),
138
+ 'col_image_name' => __('Name', 'wp-meta-seo'),
139
+ 'col_image_info' => __('Optimization Info', 'wp-meta-seo'),
140
+ 'col_image_alternative' => __('Alternative text', 'wp-meta-seo'),
141
+ 'col_image_title' => __('Title', 'wp-meta-seo'),
142
+ 'col_image_legend' => __('Legend', 'wp-meta-seo'),
143
+ 'col_image_desc' => __('Description', 'wp-meta-seo'),
144
+ );
145
+ }
146
+
147
+ function get_sortable_columns() {
148
+ return $sortable = array(
149
+ 'col_image_name' => array('post_name', true),
150
+ 'col_image_title' => array('post_title', true),
151
+ );
152
+ }
153
+
154
+
155
+ /**
156
+ * Print column headers, accounting for hidden and sortable columns.
157
+ *
158
+ * @since 3.1.0
159
+ * @access public
160
+ *
161
+ * @param bool $with_id Whether to set the id attribute or not
162
+ */
163
+ public function print_column_headers( $with_id = true ) {
164
+ list( $columns, $hidden, $sortable ) = $this->get_column_info();
165
+
166
+ $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
167
+ $current_url = remove_query_arg( 'paged', $current_url );
168
+
169
+ if ( isset( $_GET['orderby'] ) )
170
+ $current_orderby = $_GET['orderby'];
171
+ else
172
+ $current_orderby = '';
173
+
174
+ if ( isset( $_GET['order'] ) && 'desc' == $_GET['order'] )
175
+ $current_order = 'desc';
176
+ else
177
+ $current_order = 'asc';
178
+
179
+ if ( ! empty( $columns['cb'] ) ) {
180
+ static $cb_counter = 1;
181
+ $columns['cb'] = '<label class="screen-reader-text" for="cb-select-all-' . $cb_counter . '">' . __( 'Select All' ) . '</label>'
182
+ . '<input id="cb-select-all-' . $cb_counter . '" type="checkbox" />';
183
+ $cb_counter++;
184
+ }
185
+
186
+ foreach ( $columns as $column_key => $column_display_name ) {
187
+ $class = array( 'manage-column', "column-$column_key" );
188
+
189
+ $style = '';
190
+ if ( in_array( $column_key, $hidden ) )
191
+ $style = 'display:none;';
192
+
193
+ $style = ' style="' . $style . '"';
194
+
195
+ if ( 'cb' == $column_key )
196
+ $class[] = 'check-column';
197
+ elseif ( in_array( $column_key, array( 'posts', 'comments', 'links' ) ) )
198
+ $class[] = 'num';
199
+
200
+ if ( isset( $sortable[$column_key] ) ) {
201
+ list( $orderby, $desc_first ) = $sortable[$column_key];
202
+
203
+ if ( $current_orderby == $orderby ) {
204
+ $order = 'asc' == $current_order ? 'desc' : 'asc';
205
+ $class[] = 'sorted';
206
+ $class[] = $current_order;
207
+ } else {
208
+ $order = $desc_first ? 'desc' : 'asc';
209
+ $class[] = 'sortable';
210
+ $class[] = $desc_first ? 'asc' : 'desc';
211
+ }
212
+
213
+ $column_display_name = '<a href="' . esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ) . '"><span>' . $column_display_name . '</span><span class="sorting-indicator"></span></a>';
214
+ }
215
+
216
+ $id = $with_id ? "id='$column_key'" : '';
217
+
218
+ if ( !empty( $class ) )
219
+ $class = "class='" . join( ' ', $class ) . "'";
220
+
221
+ if($column_key === 'col_id'){
222
+ echo "<th scope='col' $id $class $style colspan=\"1\">$column_display_name</th>";
223
+ }
224
+ elseif($column_key === 'col_image_name'){
225
+ echo "<th scope='col' $id $class $style colspan=\"4\">$column_display_name</th>";
226
+ }
227
+ elseif($column_key === 'col_image_info'){
228
+ echo "<th scope='col' $id $class $style colspan=\"5\">$column_display_name</th>";
229
+ }
230
+ else{
231
+ echo "<th scope='col' $id $class $style colspan=\"3\">$column_display_name</th>";
232
+ }
233
+ }
234
+ }
235
+
236
+ function prepare_items() {
237
+ global $wpdb, $_wp_column_headers;
238
+ $screen = get_current_screen();
239
+
240
+ $where=array();
241
+ $post_type='attachment';
242
+ $where[] = " post_type='$post_type' ";
243
+ $where[] = " ((post_mime_type='image/jpeg') OR (post_mime_type='image/jpg') OR (post_mime_type='image/png') OR (post_mime_type='image/gif')) ";
244
+
245
+ if (!empty($_REQUEST["search"])) {
246
+ if (!empty($_REQUEST["txtkeyword"])){
247
+ $_REQUEST["txtkeyword"] = stripslashes($_REQUEST["txtkeyword"]);
248
+ $_REQUEST["txtkeyword"] = $wpdb->esc_like( $_REQUEST["txtkeyword"] );
249
+ $where[] = $wpdb->prepare(" (post_title Like %s or post_name Like %s)", "%" . $_REQUEST["txtkeyword"] . "%", "%" . $_REQUEST["txtkeyword"] . "%" );
250
+
251
+ }
252
+ }
253
+
254
+ if(!empty($_REQUEST['sldate'])){
255
+ $where[] =$wpdb->prepare(" post_date Like %s","%" .$_REQUEST['sldate']. "%");
256
+ }
257
+
258
+ $orderby = !empty($_GET["orderby"]) ? ($_GET["orderby"]) : 'post_name';
259
+ $order = !empty($_GET["order"]) ? ($_GET["order"]) : 'ASC';
260
+ if (!empty($orderby) & !empty($order)) {
261
+ $orderStr =$wpdb->prepare(' ORDER BY %s %s',$orderby,$order);
262
+ $orderStr = str_replace("'","",$orderStr);
263
+ }
264
+
265
+
266
+
267
+ $query = "SELECT ID, post_title as title, post_name as name, post_content as des, post_excerpt as legend, guid, post_type , post_mime_type, post_status, mt.meta_value AS alt
268
+ FROM $wpdb->posts as posts
269
+ LEFT JOIN (SELECT * FROM {$wpdb->prefix}postmeta WHERE meta_key = '_wp_attachment_image_alt') mt ON mt.post_id = posts.ID
270
+ WHERE ". implode(" and ", $where) .$orderStr;
271
+
272
+ $total_items = $wpdb->query($query);
273
+
274
+ if(!empty($_REQUEST['metaseo_imgs_per_page'])){
275
+ $_per_page = intval($_REQUEST['metaseo_imgs_per_page']);
276
+ }
277
+ else {
278
+ $_per_page = 0;
279
+ }
280
+ $per_page = get_user_option('metaseo_imgs_per_page');
281
+ if( $per_page !== false) {
282
+ if($_per_page && $_per_page !== $per_page ){
283
+ $per_page = $_per_page;
284
+ update_user_option(get_current_user_id(), 'metaseo_imgs_per_page', $per_page);
285
+ }
286
+ }
287
+ else{
288
+ if($_per_page > 0) {
289
+ $per_page = $_per_page;
290
+ }
291
+ else { $per_page = 10; }
292
+ add_user_meta(get_current_user_id(), 'metaseo_imgs_per_page', $per_page);
293
+ }
294
+
295
+ $paged = !empty($_GET["paged"]) ? ($_GET["paged"]) : '';
296
+
297
+ if (empty($paged) || !is_numeric($paged) || $paged <= 0) {
298
+ $paged = 1;
299
+ }
300
+
301
+ $total_pages = ceil($total_items / $per_page);
302
+
303
+ if (!empty($paged) && !empty($per_page)) {
304
+ $offset = ($paged - 1) * $per_page;
305
+ $query .= ' LIMIT ' . (int) $offset . ',' . (int) $per_page;
306
+ }
307
+
308
+ $this->set_pagination_args(array(
309
+ 'total_items' => $total_items,
310
+ 'total_pages' => $total_pages,
311
+ 'per_page' => $per_page
312
+ ));
313
+
314
+ $columns = $this->get_columns();
315
+ $hidden = array();
316
+ $sortable = $this->get_sortable_columns();
317
+ $this->_column_headers = array($columns, $hidden, $sortable);
318
+ $this->items = $wpdb->get_results($query);
319
+
320
+ }
321
+
322
+ function search_box1( ) {
323
+ if (empty($_REQUEST['txtkeyword']) && !$this->has_items())
324
+ return;
325
+ $txtkeyword = (!empty($_REQUEST['txtkeyword'])) ? urldecode(stripslashes($_REQUEST['txtkeyword']) ) : "";
326
+ if (!empty($_REQUEST['orderby']))
327
+ echo '<input type="hidden" name="orderby" value="' . esc_attr($_REQUEST['orderby']) . '" />';
328
+ if (!empty($_REQUEST['order']))
329
+ echo '<input type="hidden" name="order" value="' . esc_attr($_REQUEST['order']) . '" />';
330
+ if (!empty($_REQUEST['post_mime_type']))
331
+ echo '<input type="hidden" name="post_mime_type" value="' . esc_attr($_REQUEST['post_mime_type']) . '" />';
332
+ if (!empty($_REQUEST['detached']))
333
+ echo '<input type="hidden" name="detached" value="' . esc_attr($_REQUEST['detached']) . '" />';
334
+ ?>
335
+ <p class="search-box">
336
+
337
+ <input type="search" id="image-search-input" name="txtkeyword" value="<?php echo esc_attr(stripslashes($txtkeyword)); ?>" />
338
+ <?php submit_button('Search', 'button', 'search', false, array('id' => 'search-submit')); ?>
339
+ </p>
340
+ <?php
341
+ }
342
+
343
+
344
+ function months_fillter($post_type, $name,$namebutton) {
345
+ global $wpdb, $wp_locale;
346
+
347
+ $where = " AND ((post_mime_type='image/jpeg') OR (post_mime_type='image/jpg') OR (post_mime_type='image/png') OR (post_mime_type='image/gif')) ";
348
+ $months = $wpdb->get_results($wpdb->prepare("
349
+ SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month
350
+ FROM $wpdb->posts
351
+ WHERE post_type = %s".$where."
352
+ ORDER BY post_date DESC
353
+ ", $post_type));
354
+
355
+ $months = apply_filters('months_dropdown_results', $months, $post_type);
356
+
357
+ $month_count = count($months);
358
+
359
+ if (!$month_count || ( 1 == $month_count && 0 == $months[0]->month ))
360
+ return;
361
+
362
+ $m = isset($_REQUEST['sldate']) ? $_REQUEST['sldate'] : 0;
363
+ ?>
364
+ <label for="filter-by-date" class="screen-reader-text"><?php _e('Filter by date'); ?></label>
365
+ <select name="<?php echo $name ?>" id="filter-by-date" class="metaseo-filter">
366
+ <option<?php selected($m, 0); ?> value="0"><?php _e('All dates'); ?></option>
367
+ <?php
368
+ foreach ($months as $arc_row) {
369
+
370
+ if (0 == $arc_row->year) continue;
371
+ $month = zeroise($arc_row->month, 2);
372
+ $year = $arc_row->year;
373
+ printf("<option %s value='%s' >%s</option>\n",
374
+ selected($m, "$year-$month", false),
375
+ esc_attr("$arc_row->year-$month"),
376
+ sprintf(__('%1$s %2$d'), $wp_locale->get_month($month), $year)
377
+ );
378
+ }
379
+ ?>
380
+ </select>
381
+
382
+ <?php
383
+ submit_button(__('Filter'), 'button', $namebutton, false, array('id' => 'image-submit'));
384
+ }
385
+
386
+ function display_rows() {
387
+ $url = URL;
388
+ $url = preg_replace('/(^(http|https):\/\/[w]*\.*)/', '', $url);
389
+ $records = $this->items;
390
+ $i = 0;
391
+ $alternate = "";
392
+
393
+ list( $columns, $hidden ) = $this->get_column_info();
394
+
395
+ if (!empty($records)) {
396
+ foreach ($records as $rec) {
397
+ $alternate = 'alternate' == $alternate ? '' : 'alternate';
398
+ $i++;
399
+ $classes = $alternate;
400
+ $img_meta = get_post_meta($rec->ID, '_wp_attachment_metadata', TRUE);
401
+ $thumb = wp_get_attachment_image_src($rec->ID, 'thumbnail' );
402
+ if(!$thumb) {
403
+ $thumb_url = $rec->guid;
404
+ }else {
405
+ $thumb_url = $thumb['0'];
406
+ }
407
+
408
+ if (strrpos($img_meta['file'], '/') !== false) {
409
+ $img_name = substr($img_meta['file'], strrpos($img_meta['file'], '/') + 1);
410
+ } else {
411
+ $img_name = $img_meta['file'];
412
+ }
413
+ $type = substr($img_meta['file'], strrpos($img_meta['file'], '.'));
414
+ $img_name = str_replace($type, '', $img_name);
415
+
416
+ $upload_dir = wp_upload_dir();
417
+ $img_path = $upload_dir['basedir'] . '/' . $img_meta['file'];
418
+ if (is_readable($img_path)) {
419
+ //Get image attributes including width and height
420
+ list($img_width, $img_height,$img_type) = getimagesize($img_path);
421
+ //Get image size
422
+ if (($size = filesize($img_path) / 1024) > 1024) {
423
+ $img_size = ($size / 1024);
424
+ $img_sizes = ' MB';
425
+ } else {
426
+ $img_size = ($size);
427
+ $img_sizes = ' KB';
428
+ }
429
+ $img_size=round($img_size,1);
430
+ //Get the date that image was uploaded
431
+ $img_date = get_the_date("", $rec->ID);
432
+ }
433
+
434
+ echo '<tr id="record_' . $rec->ID . '" class="' . $classes . '" >';
435
+
436
+ foreach ($columns as $column_name => $column_display_name) {
437
+
438
+ $class = sprintf('class="%1$s column-%1$s"', $column_name);
439
+ $style = "";
440
+
441
+ if (in_array($column_name, $hidden)) {
442
+ $style = ' style="display:none;"';
443
+ }
444
+
445
+ $attributes = $class . $style;
446
+
447
+ switch ($column_name) {
448
+ case 'col_id':
449
+ echo '<td class="col_id" colspan="1">';
450
+ echo $i;
451
+ echo '</td>';
452
+ break;
453
+
454
+ case 'col_image':
455
+ $img = sprintf("<img src='$thumb_url' width='100px' height='100px' class=\"metaseo-image\" data-name=\"$img_name$type\" data-img-post-id=\"$rec->ID\" />");
456
+
457
+ echo sprintf('<td %2$s colspan="3">%1$s</td>', $img, $attributes);
458
+ break;
459
+
460
+ case 'col_image_name':
461
+ $info = '<div class="img-name-wrapper">';
462
+ $info .= '<input type="text" name="name_image['.$rec->ID.']" class="metaseo-img-meta metaseo-img-name" data-meta-type="change_image_name" id="img-name-'.$rec->ID.'" data-post-id="'.$rec->ID.'" size="12" value="'.$img_name.'" data-extension="'.$type.'" /><span class="img_type">'.$type.'</span>';
463
+ #$info .= '<p class="savedInfo" style="display:none;"></p>';
464
+ $info .= '<p>size: ' . $img_size . $img_sizes. '</p>';
465
+ $info .= '<p>' . $img_width . 'x' . $img_height . '</p>';
466
+ $info .= '<p>' . $img_date . '</p>';
467
+ $info .= '<span class="saved-info" style="position:relative">
468
+ <span class="meta-update" style="position:absolute"></span>
469
+ </span>';
470
+ $info .= '</div>';
471
+ echo sprintf('<td %2$s colspan="4">%1$s</td>', $info, $attributes);
472
+ break;
473
+
474
+ case 'col_image_info':
475
+ $icon = '<img src= "' . WPMETASEO_PLUGIN_URL . 'img/view.png" />';
476
+ $icon = '';
477
+ $info = "<div class=\"opt-info\" id=\"opt-info-$rec->ID\"></div>";
478
+ $info .= '<span class="metaseo-loading"></span>';
479
+ $info .= '
480
+ <div class="popup-bg"></div>
481
+ <div class="popup post-list">
482
+ <span class="popup-close" title="Close">x</span>
483
+ <div class="popup-content"></div>
484
+ </div>
485
+ ';
486
+
487
+ echo sprintf('<td %2$s colspan="5" style="position:relative">%1$s</td>', $info, $attributes);
488
+ break;
489
+
490
+ case 'col_image_alternative':
491
+ $input = ("<input name='img_alternative[$rec->ID]' class='metaseo-img-meta' data-meta-type='alt_text' id='img-alt-$rec->ID' data-post-id='$rec->ID' size='13' value='". esc_attr($rec->alt)."' type='text'>");
492
+ $input .= ('<span class="saved-info" style="position:relative">
493
+ <span class="meta-update" style="position:absolute"></span>
494
+ </span>');
495
+ echo sprintf('<td %2$s colspan="3">%1$s</td>', $input, $attributes);
496
+ break;
497
+
498
+ case 'col_image_title':
499
+ $input = ("<input name='img_title[$rec->ID]' class='metaseo-img-meta' data-meta-type='image_title' id='img-title-$rec->ID' data-post-id='$rec->ID' size='13' value='".esc_attr($rec->title)."' type='text'>");
500
+ $input .= ('<span class="saved-info" style="position:relative">
501
+ <span class="meta-update" style="position:absolute"></span>
502
+ </span>');
503
+ echo sprintf('<td %2$s colspan="3">%1$s</td>', $input, $attributes);
504
+ break;
505
+
506
+ case 'col_image_legend':
507
+ $input = ("<input name='img_legend[$rec->ID]' class='metaseo-img-meta' data-meta-type='image_caption' id='img-legend-$rec->ID' data-post-id='$rec->ID' value='".esc_attr($rec->legend)."' size='13' type='text'>");
508
+ $input .= ('<span class="saved-info" style="position:relative">
509
+ <span class="meta-update" style="position:absolute"></span>
510
+ </span>');
511
+ echo sprintf('<td %2$s colspan="3">%1$s</td>', $input, $attributes);
512
+ break;
513
+
514
+ case 'col_image_desc':
515
+ $input = ("<input name='img_desc[$rec->ID]' class='metaseo-img-meta' data-meta-type='image_description' id='img-desc-$rec->ID' data-post-id='$rec->ID' size='30' value='".esc_attr($rec->des)."' type='text'>");
516
+ $input .= ('<span class="saved-info" style="position:relative">
517
+ <span class="meta-update" style="position:absolute"></span>
518
+ </span>');
519
+ echo sprintf('<td %2$s colspan="3">%1$s</td>', $input, $attributes);
520
+ break;
521
+ }
522
+
523
+ }
524
+
525
+ echo '</tr>';
526
+ }
527
+ }
528
+ }
529
+
530
+ public static function add_more_attachment_sizes_js($response, $attachment){
531
+ $metaseo_imgs_sizes = get_post_meta($attachment->ID, '_metaseo_sizes_optional', true);
532
+
533
+ if(!empty($metaseo_imgs_sizes)){
534
+ foreach($metaseo_imgs_sizes as $key => $size){
535
+ $response['sizes'][$key] = $size;
536
+ }
537
+ }
538
+
539
+ return $response;
540
+ }
541
+
542
+ public static function add_more_attachment_sizes_choose($sizes){
543
+ global $wpdb;
544
+ $query = "SELECT `meta_value` FROM $wpdb->postmeta WHERE `meta_key` = '_metaseo_sizes_optional' AND `meta_value` <> ''";
545
+
546
+ $metaseo_imgs_sizes = $wpdb->get_results($query);
547
+ if(!empty($metaseo_imgs_sizes)){
548
+ $_sizes = array();
549
+ foreach($metaseo_imgs_sizes as $metaseo_img_sizes){
550
+ $metaseo_img_sizes = @unserialize($metaseo_img_sizes->meta_value);
551
+ foreach($metaseo_img_sizes as $key => $size){
552
+ add_image_size($key, $size['width'], $size['height'], false);
553
+ }
554
+ }
555
+
556
+ }
557
+
558
+ $new_sizes = array();
559
+
560
+ $added_sizes = get_intermediate_image_sizes();
561
+
562
+ // $added_sizes is an indexed array, therefore need to convert it
563
+ // to associative array, using $value for $key and $value
564
+ foreach( $added_sizes as $value) {
565
+ if(strpos($value, '-metaseo') !== false){
566
+ $_value = substr($value, 0, strrpos($value, '-metaseo'));
567
+ }else{
568
+ $_value = $value;
569
+ }
570
+ $new_sizes[$value] = ucwords(str_replace(array('-','_'), ' &ndash; ', $_value));
571
+ }
572
+
573
+ // This preserves the labels in $sizes, and merges the two arrays
574
+ $new_sizes = array_merge( $new_sizes, $sizes );
575
+
576
+ return $new_sizes;
577
+ }
578
+
579
+ private static function display_fix_metas_list($img_post_id, $posts, $meta_counter, $p, $im){
580
+ if($meta_counter){
581
+ $header = __('We found ' . $meta_counter . $im . $p . 'which needed to add or change meta information', 'wp-meta-seo');
582
+ }else{
583
+ $header = __('We found 0 image which needed to add or change meta information', 'wp-meta-seo');
584
+ }
585
+
586
+ //Get default meta information of the image
587
+ $img_post = get_post($img_post_id);
588
+ $alt = get_post_meta($img_post_id, '_wp_attachment_image_alt', true);
589
+ $title = $img_post->post_title;
590
+ ?>
591
+ <h3 class="content-header"><?php echo $header ?></h3>
592
+ <div class="content-box">
593
+ <table class="wp-list-table widefat fixed posts">
594
+ <thead></thead>
595
+ <tbody>
596
+ <?php $alternate = '';?>
597
+ <?php if(count($posts) < 1): ?>
598
+ <tr><td colspan="10" style="height:95%"><?php echo __('This image has still not been inserted in any post!', 'wp-meta-seo') ?></td></tr>
599
+ <?php else: ?>
600
+ <tr class="metaseo-border-bottom">
601
+ <td colspan="1">ID</td>
602
+ <td colspan="2">Title</td>
603
+ <td colspan="2">Image</td>
604
+ <td colspan="5">Image Meta</td>
605
+ </tr>
606
+ <?php foreach($posts as $post): ?>
607
+ <?php foreach($post['meta'] as $k => $meta): ?>
608
+ <?php $alternate = 'alternate' == $alternate ? '' : 'alternate';
609
+ $file_name = substr($meta['img_src'], strrpos($meta['img_src'], '/')+1);
610
+ ?>
611
+ <tr class="<?php echo $alternate ?>">
612
+ <td colspan="1"><?php echo $post['ID'] ?></td>
613
+ <td colspan="2">
614
+ <p><?php echo $post['title'] ?></p>
615
+ </td>
616
+ <td colspan="2">
617
+ <div class="metaseo-img-wrapper">
618
+ <img src="<?php echo $meta['img_src'] ?>" />
619
+ </div>
620
+ </td>
621
+ <td colspan="5">
622
+ <?php foreach($meta['type'] as $type => $value): ?>
623
+ <div class="metaseo-img-wrapper">
624
+ <?php
625
+ $specialChr = array('"', '\'');
626
+ foreach($specialChr as $chr){
627
+ $value = str_replace($chr, htmlentities2($chr), $value);
628
+ }
629
+ ?>
630
+ <input type="text" value="<?php echo ($value != '' ? $value : ''); ?>" id="metaseo-img-<?php echo $type. '-' .$post['ID'] ?>" class="metaseo-fix-meta metaseo-img-<?php echo $type ?>" data-meta-key="_metaseo_fix_metas" data-post-id="<?php echo $post['ID'] ?>" data-img-post-id="<?php echo $img_post_id ?>" data-meta-type="<?php echo $type ?>" data-meta-order="<?php echo $k ?>" data-file-name="<?php echo $file_name; ?>" placeholder="<?php echo ($value == '' ? __(ucfirst($type) . ' is empty', 'wp-meta-seo') : '') ?>" onfocus="metaseo_fix_meta(this);" onblur="updateInputBlur(this)" onkeydown="return checkeyCode(event,this)" />
631
+ <span class="meta-update"></span>
632
+ <?php if(trim($$type) != '' && trim($$type) != $value): ?>
633
+ <a class="button meta-default" href="#" data-default-value="<?php echo esc_attr($$type) ?>" title="Add to input box" onclick="add_meta_default(this)"> <?php echo '<img src= "' . WPMETASEO_PLUGIN_URL . 'img/img-arrow.png" />' ?> Copy </a>
634
+ <span class="img_seo_type"><?php echo $$type; ?></span>
635
+ <?php endif ?>
636
+ </div>
637
+ <?php endforeach ?>
638
+ <span class="saved-info"></span>
639
+ </td>
640
+ </tr>
641
+ <?php endforeach ?>
642
+ <?php endforeach ?>
643
+ <?php endif ?>
644
+ </tbody>
645
+ <tfoot></tfoot>
646
+ </table>
647
+ </div>
648
+ <div style="padding:5px"></div>
649
+ <?php
650
+
651
+ }
652
+
653
+ private static function display_resize_image_list($img_post_id,$posts, $img_counter, $p, $im){
654
+
655
+ $header = __('We found ' . $img_counter . $im . $p . ' which needed to optimize', 'wp-meta-seo');
656
+ ?>
657
+ <h3 class="content-header"><?php echo $header ?></h3>
658
+ <div class="content-box">
659
+ <table class="wp-list-table widefat fixed posts">
660
+ <thead></thead>
661
+ <tbody>
662
+ <tr class="metaseo-border-bottom">
663
+ <td colspan="1">ID</td>
664
+ <td colspan="3">Title</td>
665
+ <td colspan="4">Current Images</td>
666
+ <td colspan="2" class="metaseo-action">Action</td>
667
+ <td colspan="4">After Replacing</td>
668
+ </tr>
669
+ <?php $alternate ="";
670
+ foreach($posts as $post): ?>
671
+ <?php $alternate = 'alternate' == $alternate ? '' : 'alternate'; ?>
672
+ <tr class="<?php echo $alternate ?>">
673
+ <td colspan="1"><?php echo $post['ID'] ?></td>
674
+ <td colspan="3">
675
+ <p><?php echo $post['title'] ?></p>
676
+ </td>
677
+ <td colspan="4" style="overflow: hidden;">
678
+ <?php foreach($post['img_before_optm'] as $key => $src):?>
679
+ <div class="metaseo-img-wrapper">
680
+ <div class="metaseo-img">
681
+ <img width="<?php echo @$src['width'] ;?>" height="<?php #echo @$src['height'] ;?>" src="<?php echo $src['src'] ?>" />
682
+ <div class="img-choosen">
683
+ <input type="checkbox" checked="true" class="metaseo-checkin checkin-<?php echo $post['ID'] ?>" value="<?php echo $key ?>" id="checkin-<?php echo $post['ID'].'-'.$key ?>" onclick="uncheck(this)" />
684
+ </div>
685
+ <p class="metaseo-msg"></p>
686
+ </div>
687
+ <div class="dimension">
688
+ Orig. </br>
689
+ <span>Dimensions</span>: <?php echo $src['dimension'] ?></br>
690
+ <span>File size</span>: <?php echo $src['size'].' '.$src['sizes'] ?>
691
+ </div>
692
+ </div>
693
+ <?php endforeach ?>
694
+ </td>
695
+ <td colspan="2" class="metaseo-action">
696
+ <a href="javascript:void(0);" class="metaseo-optimize button" data-img-post-id="<?php echo $img_post_id ?>" data-post-id="<?php echo $post['ID'] ?>" onclick="optimize_imgs(this)"><?php echo __('Replace?', 'wp-meta-seo') ?></a>
697
+ <span class="optimizing spinner"></span>
698
+ </td>
699
+ <td colspan="4">
700
+ <?php foreach($post['img_after_optm'] as $src):?>
701
+ <div class="metaseo-img-wrapper">
702
+ <div class="metaseo-img">
703
+ <img src="<?php echo $src['src'] ?>" />
704
+ </div>
705
+ <div class="dimension">
706
+ OPT </br>
707
+ <span>Dimensions</span>: <?php echo $src['dimension'] ?></br>
708
+ <span>File size</span>: <?php echo $src['size'].' '.$src['sizes'] ?>
709
+ </div>
710
+ </div>
711
+ <?php endforeach ?>
712
+ </td>
713
+ </tr>
714
+
715
+ <?php endforeach ?>
716
+ <tr class="metaseo-border-top">
717
+ <td colspan="8"></td>
718
+ <td colspan="2">
719
+ <a href="javascript:void(0);" id="metaseo-replace-all" class="button button-primary" onclick="optimize_imgs_group(this)">
720
+ <?php echo __('Replace All') ?>
721
+ </a>
722
+ <span class="optimizing spinner"></span>
723
+ </td>
724
+ <td colspan="4"></td>
725
+ </tr>
726
+ </tbody>
727
+ <tfoot></tfoot>
728
+ </table>
729
+ </div>
730
+ <div style="padding:5px"></div>
731
+ <?php
732
+ }
733
+
734
+ public static function optimizeImages(){
735
+ if(!empty($_POST['post_id']) && !empty($_POST['img_post_id'])){
736
+ $post_id = intval($_POST['post_id']);
737
+ $img_post_id = intval($_POST['img_post_id']);
738
+ if(!empty($_POST['img_exclude'])){
739
+ $img_exclude = $_POST['img_exclude'];
740
+ }
741
+ else{
742
+ $img_exclude = array();
743
+ }
744
+
745
+ $ret = ImageHelper::_optimizeImages($post_id, $img_post_id, $img_exclude);
746
+ }
747
+ else{
748
+ $ret = array(
749
+ 'success' => false,
750
+ 'msg' => __('The post is not existed, please choose one another!', 'wp-meta-seo')
751
+ );
752
+ }
753
+
754
+ echo json_encode($ret);
755
+ wp_die();
756
+
757
+ }
758
+
759
+ public function process_action() {
760
+ global $wpdb;
761
+ $current_url = set_url_scheme('http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
762
+ $redirect = false;
763
+
764
+ if (isset($_POST['search']) and $_POST['search'] === 'Search') {
765
+ $current_url = add_query_arg(array("search" => "Search", "txtkeyword" => urlencode(stripslashes($_POST["txtkeyword"])) ) , $current_url);
766
+ $redirect = true;
767
+ }
768
+
769
+ if (isset($_POST['filter_date_action']) and $_POST['filter_date_action'] === 'Filter') {
770
+ $current_url = add_query_arg(array("sldate" => $_POST["sldate"]), $current_url);
771
+ $redirect = true;
772
+ }
773
+
774
+ if(!empty($_POST['paged'])){
775
+ $current_url = add_query_arg(array( "paged" => intval($_POST['paged'])), $current_url);
776
+ $redirect = true;
777
+ }
778
+
779
+ if(!empty($_POST['metaseo_imgs_per_page'])){
780
+ $current_url = add_query_arg(array( "metaseo_imgs_per_page" => intval($_POST['metaseo_imgs_per_page'])), $current_url);
781
+ $redirect = true;
782
+ }
783
+
784
+ if($redirect === true){
785
+ wp_redirect($current_url);
786
+ ob_end_flush();
787
+ exit();
788
+ }
789
+ }
790
+
791
+ public static function load_posts_callback(){
792
+ global $wpdb;
793
+ $_POST = stripslashes_deep( $_POST );
794
+ $post_id = intval($_POST['post_id']);
795
+ $img = trim($_POST['img_name']);
796
+ $opt_key = strtolower(trim($_POST['opt_key']));
797
+ $btn = preg_replace('/[_]+/i', ' ',$opt_key);
798
+ $btn = ucwords($btn);
799
+ $gotIt = false;
800
+
801
+ if($post_id && !empty($img) && !empty($opt_key)){
802
+ $fn = "display_{$opt_key}_list";
803
+ if(method_exists('MetaSeo_Image_List_Table', $fn)){
804
+ //Get list of posts contain this image and its clones
805
+ $posts = ImageHelper::_get_post_list($post_id, $opt_key);
806
+
807
+ if(count($posts) > 0){
808
+ $img_counter = 0;
809
+ //Now the time to resize the images
810
+ if($opt_key === 'resize_image'){
811
+ $upload_dir = wp_upload_dir();
812
+ $metaseo_sizes_optional = get_post_meta($post_id, '_metaseo_sizes_optional', TRUE);
813
+ if(!is_array($metaseo_sizes_optional)){ $metaseo_sizes_optional = array(); }
814
+ $attachment_meta_data = wp_get_attachment_metadata($post_id);
815
+
816
+ foreach($posts as &$post){
817
+ foreach($post['img_after_optm'] as &$img){
818
+ $img_counter++;
819
+ $destination = $upload_dir['basedir'] . '/' . $img['path'];
820
+ if(@ImageHelper::IResize($img['src_origin'], $img['width'], $img['height'], $destination)){
821
+
822
+ $size = (filesize($destination)/1024);
823
+ if($size>1024){
824
+ $size=$size/1024;
825
+ $sizes = 'MB';
826
+ }else{
827
+ $sizes = 'KB';
828
+ }
829
+ $size=@round($size,1);
830
+ $img['size'] = $size;
831
+ $img['sizes'] = $sizes;
832
+ }
833
+
834
+ $kpart = ImageHelper::IGetPart($img['path']);
835
+ $key = preg_replace('/\-(\d+)x(\d+)$/i', '-metaseo${1}${2}', $kpart->name);
836
+ $key = strtolower($key);
837
+ $file = substr($img['path'], strrpos($img['path'], '/') + 1);
838
+ if(!in_array($key, array_keys($metaseo_sizes_optional))){
839
+ $metaseo_sizes_optional[$key] = array(
840
+ 'url' => $img['src'],
841
+ 'width' => $img['width'],
842
+ 'height' => $img['height'],
843
+ 'orientation' => 'landscape',
844
+ );
845
+ }
846
+
847
+ if(!isset($attachment_meta_data['sizes'][$key])){
848
+ $attachment_meta_data['sizes'][$key] = array(
849
+ 'file' => $file,
850
+ 'width' => $img['width'],
851
+ 'height' => $img['height'],
852
+ 'mime-type' => 'image/jpeg'
853
+ );
854
+ }
855
+
856
+ }
857
+ }
858
+
859
+ wp_update_attachment_metadata($post_id, $attachment_meta_data);
860
+ update_post_meta($post_id, '_metaseo_sizes_optional', $metaseo_sizes_optional);
861
+ }
862
+ elseif($opt_key === 'fix_metas'){
863
+ $toEdit = false;
864
+ $pIDs = array();
865
+ foreach($posts as $ID => &$post){
866
+ $img_counter += count($post['meta']);
867
+ foreach($post['meta'] as $order => $meta){
868
+ if($meta['type']['alt'] == '' || $meta['type']['title'] == ''){
869
+ $toEdit = true;
870
+ }
871
+
872
+ if($meta['type']['alt'] != '' && $meta['type']['title'] != ''){
873
+ $pIDs[$ID][] = $order;
874
+ }
875
+ }
876
+ }
877
+
878
+ if($toEdit === true){
879
+ foreach($pIDs as $ID => $orders){
880
+ foreach($orders as $order){
881
+ unset($posts[$ID]['meta'][$order]);
882
+ if($img_counter > 0){ $img_counter--; }
883
+ }
884
+
885
+ if(empty($posts[$ID]['meta'])){ unset($posts[$ID]); }
886
+ }
887
+ }
888
+ }
889
+ //-----------------------------
890
+ }
891
+
892
+ }
893
+ }
894
+
895
+ //This is a bit crazy but could give more exact information
896
+
897
+ if(count($posts) > 1){
898
+ $p = ' in ' . count($posts) . ' posts ';
899
+ }
900
+ else{
901
+ $p = '';
902
+ }
903
+
904
+ if(isset($img_counter) and $img_counter > 1){
905
+ $im = ' images ';
906
+ }
907
+ else{
908
+ if(!isset($img_counter)) { $img_counter = 0; }
909
+ $im = ' image ';
910
+ }
911
+
912
+ self::$fn($post_id, $posts, $img_counter, $p, $im);
913
+ wp_die();
914
+
915
+ }
916
+
917
+ public static function scan_posts_callback(){
918
+ $_POST = stripslashes_deep( $_POST );
919
+ $imgs = $_POST['imgs'];
920
+ if(!empty($imgs)){
921
+ if(!is_array($imgs)){
922
+ $ret['success'] = false;
923
+ $ret['msg'] = 'No images are available, please check again!';
924
+ return $ret;
925
+ }
926
+
927
+ $_imgs = array();
928
+ foreach($imgs as $key => $img){
929
+ if(empty($img['img_post_id']) || empty($img['name'])){
930
+ continue;
931
+ }
932
+
933
+ $_imgs[trim($img['name'])] = $img['img_post_id'];
934
+ }
935
+ unset($imgs);
936
+
937
+ if(!count($_imgs)){
938
+ $ret['success'] = false;
939
+ $ret['msg'] = 'No images are available, please check again!';
940
+ return $ret;
941
+ }
942
+
943
+ $msg = ImageHelper::IScanPosts($_imgs, true);
944
+ $ret['msg'] = $msg;
945
+ $ret['success'] = true;
946
+ }
947
+
948
+ else{
949
+ $ret['success'] = false;
950
+ $ret['msg'] = 'No images are available, please check again!';
951
+ }
952
+
953
+ echo json_encode($ret);
954
+ wp_die();
955
+ }
956
+
957
+ public static function updateMeta_callback(){
958
+ global $wpdb;
959
+ $response = new stdClass();
960
+ $response->updated = false;
961
+
962
+ if(!empty($_POST['addition']['meta_key'])){
963
+ self::updateImgMeta_call_back($_POST['addition']);
964
+ }
965
+
966
+ if(!empty($_POST['meta_type']) and $_POST['meta_type'] == 'change_image_name'){
967
+ self::updateImageName_callback($_POST);
968
+ }
969
+
970
+ if(!empty($_POST['meta_type']) && !empty($_POST['post_id'])){
971
+ $meta_type = strtolower(trim($_POST['meta_type']));
972
+ $post_id = intval($_POST['post_id']);
973
+
974
+ if(!isset($_POST['meta_value'])){
975
+ $meta_value = '';
976
+ }
977
+ else{
978
+ $meta_value = trim($_POST['meta_value']);
979
+ if(preg_match('/[<>\/\'\"]+/', $meta_value)){
980
+ $response->updated = false;
981
+ $response->message = 'Should not html tag or special char';
982
+
983
+ echo json_encode($response);
984
+ wp_die();
985
+ }
986
+ }
987
+
988
+ $label = str_replace('_', ' ', $meta_type);
989
+ $label = ucfirst($label);
990
+
991
+ $aliases = array('image_title' => 'post_title', 'image_caption' => 'post_excerpt', 'image_description' => 'post_content', 'alt_text' => '_wp_attachment_image_alt');
992
+
993
+ if($meta_type != 'alt_text'){
994
+ $data = array('ID' => $post_id, $aliases[$meta_type] => $meta_value);
995
+
996
+ if(wp_update_post($data)){
997
+ $response->updated = true;
998
+ $response->msg = __($label . ' was saved', 'wp-meta-seo');
999
+ }
1000
+ }
1001
+ else{
1002
+ update_post_meta($post_id, $aliases[$meta_type], $meta_value);
1003
+ $response->updated = true;
1004
+ $response->msg = __($label . ' was saved', 'wp-meta-seo');
1005
+ }
1006
+
1007
+ }
1008
+ else{
1009
+ $response->msg = __('There is a problem when update image meta!', 'wp-meta-seo');
1010
+ }
1011
+
1012
+ echo json_encode($response);
1013
+ wp_die();
1014
+ }
1015
+
1016
+ public static function updateImageName_callback() {
1017
+
1018
+ global $wpdb;
1019
+ $postID = (int) $_POST['post_id'];
1020
+ $name = trim($_POST['meta_value']);
1021
+ $iname = preg_replace('/(\s{1,})/', '-', $name);
1022
+ $img_meta = get_post_meta($postID, '_wp_attachment_metadata', TRUE);
1023
+ $linkold = $img_meta['file'];
1024
+ $response = new stdClass();
1025
+ $response->updated = FALSE;
1026
+ $response->msg = __('There is a problem when update image name', 'wp-meta-seo');
1027
+
1028
+ $upload_dirs = wp_upload_dir();
1029
+ $upload_dir = $upload_dirs['basedir'];
1030
+ $oldpart = ImageHelper::IGetPart($linkold);
1031
+ $old_name = $oldpart->name;
1032
+
1033
+ if ($name !== "") {
1034
+ if (file_exists($upload_dir . "/" . $linkold)) {
1035
+ $newFileName = $oldpart->base_path . $iname . $oldpart->ext;
1036
+ #if ((!file_exists($upload_dir . "/" . $newFileName)) && $check==0) {
1037
+ if(!file_exists($upload_dir . "/" . $newFileName)){
1038
+ if (rename($upload_dir . "/" . $linkold, $upload_dir . "/" . $newFileName)) {
1039
+ $post_title = get_the_title($postID);
1040
+ $data_post = array('ID' => $postID, 'post_name' => $name);
1041
+ //if (wp_update_post($data_post)) {
1042
+ $where = array('ID' => $postID);
1043
+ $guid = $upload_dirs['baseurl'] . "/" . $newFileName;
1044
+ if(!$post_title){
1045
+ $id = $wpdb->update($wpdb->posts, array('guid' => $guid, 'post_title' => $name, 'post_name' => strtolower($iname)), $where);
1046
+ }
1047
+ else{
1048
+ $id = $wpdb->update($wpdb->posts, array('guid' => $guid), $where);
1049
+ }
1050
+
1051
+ if($id){
1052
+ $attached_metadata = get_post_meta($postID, "_wp_attachment_metadata", true);
1053
+ $attached_metadata["file"] = $newFileName;
1054
+
1055
+ $images_to_rename = array($oldpart->name . $oldpart->ext => $iname . $oldpart->ext);
1056
+ $old_path = $upload_dir . "/" . $linkold;
1057
+
1058
+ foreach($attached_metadata['sizes'] as &$clone){
1059
+ $clone_file_new = ImageHelper::IReplace($iname, $clone['file']);
1060
+ $clone_path = $upload_dir . '/' . $oldpart->base_path . $clone['file'];
1061
+ $clone_path_new = $upload_dir . '/' . $oldpart->base_path . $clone_file_new;
1062
+
1063
+ if( @rename($clone_path, $clone_path_new) ){
1064
+ $images_to_rename[$clone['file']] = $clone_file_new;
1065
+ $clone['file'] = $clone_file_new;
1066
+ }
1067
+
1068
+ }
1069
+
1070
+ /** Update source of this image or its clones in post contains them **/
1071
+ $query = "SELECT `ID`,`post_title`,`post_content`,`post_type`,`post_date`
1072
+ FROM $wpdb->posts
1073
+ WHERE (`post_type` = 'post' or `post_type` = 'page')
1074
+ AND `post_content` <> ''
1075
+ AND `post_content` LIKE '%<img%>%'
1076
+ ORDER BY ID";
1077
+
1078
+ $posts = $wpdb->get_results($query);
1079
+ $imgs = array($old_name . $oldpart->ext => $postID);
1080
+ $posts_contain_img = array();
1081
+ foreach($posts as $post){
1082
+ $ifound = ImageHelper::IScan($imgs, $post->post_content);
1083
+ if(count($ifound) > 0){
1084
+ $posts_contain_img[] = $post->ID;
1085
+ }
1086
+ }
1087
+
1088
+ foreach($posts_contain_img as $id){
1089
+ if($post = get_post($id)){
1090
+ foreach($images_to_rename as $src_before => $src_after){
1091
+ $src_before = '/' . $src_before;
1092
+ $src_after = '/' . $src_after;
1093
+ $post->post_content = str_replace($src_before, $src_after, $post->post_content);
1094
+ }
1095
+
1096
+ wp_update_post(array(
1097
+ 'ID' => $post->ID,
1098
+ 'post_content' => $post->post_content)
1099
+ );
1100
+ unset($post, $posts_contain_img);
1101
+ //---------------------------------
1102
+
1103
+ }
1104
+ }
1105
+ /*****************************************************/
1106
+ /** Update Image registered to Attachment sizes on Add media page**/
1107
+ $sizeOptional = get_post_meta($postID, '_metaseo_sizes_optional', true);
1108
+ $newOptional = array();
1109
+ if(!empty($sizeOptional) && is_array($sizeOptional)){
1110
+ foreach($sizeOptional as $key => $detail){
1111
+ $pattern = '/^'.strtolower($old_name).'(-metaseo\d+)$/';
1112
+ $key = preg_replace($pattern, strtolower($iname).'${1}', $key);
1113
+ $detail['url'] = ImageHelper::IReplace($iname, $detail['url']);
1114
+ $newOptional[$key] = $detail;
1115
+ }
1116
+
1117
+ update_post_meta($postID, '_metaseo_sizes_optional', $newOptional);
1118
+ unset($sizeOptional, $newOptional);
1119
+ }
1120
+ /****************************************************/
1121
+ //Need to update optimization info of this image
1122
+ ImageHelper::IScanPosts(array($iname.$oldpart->ext => $postID),true);
1123
+
1124
+ update_post_meta($postID, '_wp_attached_file', $newFileName);
1125
+ update_post_meta($postID, '_wp_attachment_metadata', $attached_metadata);
1126
+
1127
+ $response->updated = true;
1128
+ $response->msg = __('Image name was changed', 'wp-meta-seo');
1129
+ } else {
1130
+ $response->iname = $old_name;
1131
+ $response->msg = __('There is a problem when update image name', 'wp-meta-seo');
1132
+ }
1133
+ }
1134
+ } else {
1135
+
1136
+ $response->msg = __('Name is existing', 'wp-meta-seo');
1137
+ $response->iname = $old_name;
1138
+ }
1139
+ } else {
1140
+ $response->iname = $old_name;
1141
+ $response->msg = __('File is not existed', 'wp-meta-seo');
1142
+ }
1143
+ } else {
1144
+ $response->iname = $old_name;
1145
+ $response->msg = __('Should not be empty', 'wp-meta-seo');
1146
+ }
1147
+ echo json_encode($response);
1148
+ wp_die();
1149
+ }
1150
+
1151
+ public static function updateImgMeta_call_back($_post){
1152
+ global $wpdb;
1153
+ $response = new stdClass();
1154
+ $response->updated = false;
1155
+ foreach($_post as $k => $v){
1156
+ if(!$v && !in_array($k, array('meta_value', 'meta_order'))){
1157
+ $response->msg = __('There is a problem when update image meta!', 'wp-meta-seo') ;
1158
+
1159
+ echo json_encode($response);
1160
+ wp_die();
1161
+ }
1162
+ }
1163
+
1164
+ $meta_key = strtolower(trim($_post['meta_key']));
1165
+ $meta_type = strtolower(trim($_post['meta_type']));
1166
+ $meta_value = htmlspecialchars(trim($_post['meta_value']));
1167
+ $meta_order = intval($_post['meta_order']);
1168
+ $img_post_id = intval($_post['img_post_id']);
1169
+ $post_id = intval($_post['post_id']);
1170
+
1171
+ $meta = get_post_meta($img_post_id, $meta_key, true);
1172
+ //Update new value for meta info of this image in wp_postmeta
1173
+ $meta[$post_id]['meta'][$meta_order]['type'][$meta_type] = metaseo_utf8($meta_value);
1174
+ update_post_meta($img_post_id, $meta_key, $meta);
1175
+ $meta = get_post_meta($img_post_id, $meta_key, true);
1176
+
1177
+ //Then we must update this meta info in the appropriate post content
1178
+ if(!$post = get_post($post_id))
1179
+ {
1180
+ $response->msg = __('The post has been deleted before, please check again!', 'wp-meta-seo');
1181
+ }
1182
+ else{
1183
+ if($post->post_content !== ''){
1184
+ //Split content part that do not contain img tag
1185
+ $post_content_split = preg_split('/<img [^<>]+ \/>/i', $post->post_content);
1186
+ //Get all img tag from the content
1187
+ preg_match_all('/<img [^<>]+ \/>/i', $post->post_content, $matches);
1188
+ $img_tags = $matches[0];
1189
+
1190
+ if(isset($img_tags[$meta_order])){
1191
+ //&& strpos($img_tags[$meta_order], $img_src)){
1192
+ $pattern = '/'. $meta_type .'\s*?\=?\"[^\"]*\"/i';
1193
+ $replacement = $meta_type .'="'. $meta_value .'"';
1194
+ if(!preg_match($pattern, $img_tags[$meta_order], $match)){
1195
+ $pattern = '/\/>/i';
1196
+ $replacement = $meta_type .'="'. $meta_value .'" />';
1197
+ }
1198
+
1199
+ $img_tags[$meta_order] = preg_replace($pattern, $replacement, $img_tags[$meta_order]);
1200
+
1201
+ $post_content = '';
1202
+ foreach($post_content_split as $key => $split){
1203
+ if(isset($img_tags[$key])){
1204
+ $img_tag = $img_tags[$key];
1205
+ }
1206
+ else{
1207
+ $img_tag = '';
1208
+ }
1209
+
1210
+ $post_content .= $split . $img_tag;
1211
+ }
1212
+
1213
+ //Update content of this post.
1214
+ if(!wp_update_post(array('ID' => $post->ID, 'post_content' => $post_content))){
1215
+ $response->msg = __('The post haven\'t been updated, please check again!', 'wp-meta-seo');
1216
+ }
1217
+ else{
1218
+ $response->updated = true;
1219
+ $response->msg = __(ucfirst($meta_type) . ' was saved','wp-meta-seo') ;
1220
+ }
1221
+ }
1222
+ else{
1223
+ $response->msg = __('This image has been removed from the post, please check again!', 'wp-meta-seo');
1224
+ }
1225
+ }
1226
+ else{
1227
+ $response->msg = __('Content of the post is empty, please check again', 'wp-meta-seo!');
1228
+ }
1229
+ }
1230
+
1231
+
1232
+ echo json_encode($response);
1233
+ wp_die();
1234
+ }
1235
+
1236
+ }
inc/class.wp-metaseo.php ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ //Main plugin functions here
3
+ class WpMetaSeo {
4
+
5
+ private static $initiated = false;
6
+
7
+ public function __construct(){
8
+ add_action('admin_notices', array($this, 'plugin_activation_notices'));
9
+ }
10
+
11
+ public static function init() {
12
+ ob_start();
13
+ if ( ! self::$initiated ) {
14
+ self::init_hooks();
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Initializes WordPress hooks
20
+ */
21
+ private static function init_hooks() {
22
+ self::$initiated = true;
23
+ }
24
+
25
+ public static function new_title($title){
26
+ global $wp_query;
27
+ return $title;
28
+ }
29
+
30
+ /**
31
+ * Attached to activate_{ plugin_basename( __FILES__ ) } by register_activation_hook()
32
+ * @static
33
+ */
34
+ public static function plugin_activation() {
35
+ if (version_compare($GLOBALS['wp_version'], WPMETASEO_MINIMUM_WP_VERSION, '<')) {
36
+ deactivate_plugins(basename(__FILE__));
37
+ wp_die('<p>The <strong>WP Meta SEO</strong> plugin requires WordPress ' . WPMETASEO_MINIMUM_WP_VERSION . ' or higher.</p>', 'Plugin Activation Error', array('response' => 200, 'back_link' => TRUE));
38
+ }
39
+
40
+ //Set two param as flags that determine whether show import meta data from other SEO plugin button or not to 0
41
+ update_option('_aio_import_notice_flag', 0);
42
+ update_option('_yoast_import_notice_flag', 0);
43
+ update_option('plugin_to_sync_with', 0);
44
+
45
+ self::install_db();
46
+ }
47
+
48
+ /**
49
+ * Removes all connection options
50
+ * @static
51
+ */
52
+ public static function plugin_deactivation() {
53
+ //tidy up
54
+ }
55
+
56
+
57
+ public static function install_db(){
58
+ global $wpdb;
59
+ $table_name = $wpdb->prefix . 'metaseo_images';
60
+
61
+ $sql = "CREATE TABLE IF NOT EXISTS $table_name (
62
+ `id` int(11) NOT NULL AUTO_INCREMENT,
63
+ `post_id` int(11) NOT NULL,
64
+ `posts_optimized_id` text COLLATE utf8_unicode_ci NOT NULL,
65
+ `posts_need_to_optimize_id` varchar(1000) COLLATE utf8_unicode_ci NOT NULL,
66
+ `posts_prepare_to_optimize` text COLLATE utf8_unicode_ci NOT NULL,
67
+ `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
68
+ `alt_text` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
69
+ `legend` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
70
+ `description` varchar(500) COLLATE utf8_unicode_ci NOT NULL,
71
+ `link` varchar(150) COLLATE utf8_unicode_ci NOT NULL,
72
+ PRIMARY KEY (`id`)
73
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=1 ;";
74
+
75
+ require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
76
+
77
+ dbDelta( $sql );
78
+ }
79
+ }
inc/pages/content-meta.php ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /*
3
+ Meta tags content
4
+ */
5
+
6
+ if (!class_exists('MetaSeo_Content_List_Table')) {
7
+ require_once( WPMETASEO_PLUGIN_DIR . '/inc/class.metaseo-content-list-table.php' );
8
+ }
9
+
10
+ $metaseo_list_table = new MetaSeo_Content_List_Table();
11
+ $metaseo_list_table->process_action();
12
+ $metaseo_list_table->prepare_items();
13
+
14
+ if (!empty($_REQUEST['_wp_http_referer'])) {
15
+ wp_redirect(remove_query_arg(array('_wp_http_referer', '_wpnonce'), stripslashes($_SERVER['REQUEST_URI'])));
16
+ exit;
17
+ }
18
+
19
+ ?>
20
+
21
+ <div class="wrap seo_extended_table_page">
22
+ <div id="icon-edit-pages" class="icon32 icon32-posts-page"></div>
23
+
24
+ <?php echo '<h2>' . __('Content Meta', 'wp-meta-seo') . '</h2>'; ?>
25
+
26
+ <form id="wp-seo-meta-form" action="" method="post">
27
+ <?php $metaseo_list_table->display(); ?>
28
+ </form>
29
+
30
+ </div>
31
+ <script type="text/javascript">
32
+ jQuery(document).ready(function($){
33
+ $('.metaseo-metatitle').each(function() {
34
+ metaseo_updateTitle(this.id, false, false);
35
+ });
36
+ $('.metaseo-metadesc').each(function() {
37
+ metaseo_updateDesc(this.id,false);
38
+ });
39
+
40
+ $('.metaseo-metatitle').bind('input propertychange', function() {
41
+ metaseo_updateTitle(this.id, true);
42
+ });
43
+
44
+ $('.metaseo-metadesc').bind('input propertychange', function() {
45
+ metaseo_updateDesc(this.id, true);
46
+ });
47
+
48
+ $('.metaseo-metadesc, .metaseo-metatitle').bind('input propertychange', function(){
49
+ var idNumber = this.id.substr(this.id.lastIndexOf('-')+1);
50
+ if(this.id == 'metaseo-metatitle-'+idNumber){
51
+ if(!$(this).val()){
52
+ var post_title = $('#post-title-'+idNumber).text();
53
+ $('#snippet_title'+idNumber).text(post_title);
54
+ }
55
+ }
56
+
57
+ });
58
+ });
59
+ </script>
inc/pages/dashboard.php ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if (!class_exists('MetaSeo_Dashboard')) {
3
+ require_once( WPMETASEO_PLUGIN_DIR . '/inc/class.metaseo-dashboard.php' );
4
+ }
5
+
6
+ $site_name = preg_replace('/(^(http|https):\/\/[w]*\.*)/', '', get_site_url());
7
+ //$site_name = 'testdev-united.com';
8
+ $url = 'http://www.alexa.com/siteinfo/' . $site_name;
9
+ $dashboard = new MetaSeo_Dashboard();
10
+
11
+ ?>
12
+ <h1><?php echo __('DASH BOARD', 'wp-meta-seo') ?></h1>
13
+ <div class="dashboard">
14
+ <div class="left">
15
+ <div class="dashboard-left" id='dashboard-left'>
16
+ <header>
17
+ <p>
18
+ <div class="title-seo"><?php echo __('Image Meta :', 'wp-meta-seo'); ?> <span id="imgs_metas_statis"></span></div>
19
+ <div class="noload" id="imgs_metas_statis_noload">
20
+ <span class="loadtext" id="imgs_metas_statis_value"></span>
21
+ <div class="load" id="imgs_metas_statis_load">
22
+ </div>
23
+ </div>
24
+ </p>
25
+ <p>
26
+ <div class="title-seo"><?php echo __('Content Meta :', 'wp-meta-seo'); ?> <span id="metacontent"></span></div>
27
+ <div class="noload" id="imgs_metas_statis_noload">
28
+ <span class="loadtext" id="metacontent_value"></span>
29
+ <div class="load" id="metacontent_load">
30
+ </div>
31
+ </div>
32
+ </p>
33
+ <p>
34
+ <div class="title-seo"><?php echo __('Image optimization :', 'wp-meta-seo'); ?> <span id="imgs_statis"></span></div>
35
+ <div class="noload" id="imgs_metas_statis_noload">
36
+ <span class="loadtext" id="imgs_statis_value"></span>
37
+ <div class="load" id="imgs_statis_load">
38
+ </div>
39
+ </div>
40
+ </p>
41
+ </header>
42
+ <div id="canvas-holder">
43
+ <div id="chart-container">
44
+ <h2><?php echo __('Total', 'wp-meta-seo'); ?></h2>
45
+ <div style="width: 300px; margin: 0px auto; position: relative;">
46
+ <div id="avera">
47
+ </div>
48
+ <canvas id="chart-area" width="500" height="500"/>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ </div>
54
+
55
+ <div class="right">
56
+ <div class="dashboard-right">
57
+ <div id="alexa-ranking">
58
+ <?php $dashboard->displayRank($url) ?>
59
+ </div>
60
+
61
+ <div style="clear:left"></div>
62
+ <div id="wpmetaseo-update-version">
63
+ <h4><?php echo __('Latest WP Meta SEO News', 'wp-meta-seo') ?></h4>
64
+ <ul>
65
+ <li>&ndash;&nbsp<?php echo __('Version 1.0') ?></li>
66
+ <li>&ndash;&nbsp<?php echo __('Version Beta') ?></li>
67
+ </ul>
68
+ </div>
69
+ </div>
70
+ </div>
71
+
72
+ <script type="text/javascript">
73
+ jQuery(document).ready(function() {
74
+ optimizationChecking(updateChart);
75
+ var height_left = jQuery("#dashboard-left").height();
76
+ jQuery(".dashboard-right").css('height',height_left);
77
+ });
78
+
79
+
80
+ function updateChart(response) {
81
+ // console.log(response);
82
+
83
+ var columns = {
84
+ meta_desc_statis: 0,
85
+ meta_title_statis: 0,
86
+ imgs_metas_statis: 0,
87
+ imgs_statis: 0,
88
+ metacontent: 0
89
+ };
90
+
91
+ var background_color = {
92
+ imgs_metas_statis: "#FFC870",
93
+ imgs_statis: "#5AD3D1",
94
+ metacontent: "#7eb5e8"
95
+ };
96
+ for (key in response) {
97
+ // console.log(response[key]);
98
+ if(response[key][0]>0 && response[key][1]>0){
99
+ columns[key] = (response[key][0] / response[key][1]) * 100;
100
+ }else{
101
+ columns[key] = 0;
102
+ }
103
+ if(columns[key]>5){
104
+ if(columns[key] %1 !=0){
105
+ jQuery('#' + key + '_value').text(columns[key].toFixed(2) + '%').css("left", (columns[key] - 5) / 2 + "%");
106
+ }else{
107
+ jQuery('#' + key + '_value').text(columns[key] + '%').css("left", (columns[key] - 5) / 2 + "%");
108
+ }
109
+ }else{
110
+ jQuery('#' + key + '_value').text(columns[key] + '%').css("left", 0 + "%");
111
+ }
112
+ jQuery('#' + key + '_load').css({"width":columns[key] + '%','background':background_color[key]});
113
+
114
+
115
+ }
116
+ var count = columns.metacontent + columns.imgs_metas_statis + columns.imgs_statis;
117
+ var metacontent= (columns.metacontent/3);
118
+ var imgs_metas_statis= (columns.imgs_metas_statis/3);
119
+ var imgs_statis= (columns.imgs_statis/3);
120
+
121
+ var notmetacontent= 100/3 - metacontent;
122
+ var notimgs_metas_statis= 100/3 - imgs_metas_statis;
123
+ var notimgs_statis= 100/3 - imgs_statis;
124
+
125
+ var avera = ((count)/3);
126
+ jQuery('#avera').text(avera.toFixed(2) + '%').addClass('avera');
127
+
128
+ drawChart(metacontent,notmetacontent, imgs_metas_statis,notimgs_metas_statis, imgs_statis,notimgs_statis);
129
+
130
+ }
131
+ </script>
inc/pages/image-meta.php ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /*
3
+ Meta image
4
+ */
5
+
6
+ if (!class_exists('MetaSeo_Image_List_Table')) {
7
+ require_once( WPMETASEO_PLUGIN_DIR . '/inc/class.metaseo-image-list-table.php' );
8
+ }
9
+
10
+ $metaseo_list_table = new MetaSeo_Image_List_Table();
11
+ $metaseo_list_table->process_action();
12
+ $metaseo_list_table->prepare_items();
13
+
14
+ if (!empty($_REQUEST['_wp_http_referer'])) {
15
+ wp_redirect(remove_query_arg(array('_wp_http_referer', '_wpnonce'), stripslashes($_SERVER['REQUEST_URI'])));
16
+ exit;
17
+ }
18
+ ?>
19
+
20
+ <div class="wrap seo_extended_table_page">
21
+ <div id="icon-edit-pages" class="icon32 icon32-posts-page"></div>
22
+
23
+ <?php echo '<h2>' . __('Image Meta', 'wp-meta-seo') . '</h2>'; ?>
24
+
25
+ <form id="wp-seo-meta-form" action="" method="post">
26
+
27
+ <?php $metaseo_list_table->search_box1(); ?>
28
+
29
+ <?php $metaseo_list_table->display(); ?>
30
+ </form>
31
+
32
+ </div>
33
+ <script type="text/javascript">
34
+ jQuery(document).ready(function(){
35
+ //Scan all posts to find a group of images in their content
36
+ metaSeoScanImages();
37
+ });
38
+ </script>
js/Chart.js ADDED
@@ -0,0 +1,3379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*!
2
+ * Chart.js
3
+ * http://chartjs.org/
4
+ * Version: 1.0.1-beta.4
5
+ *
6
+ * Copyright 2014 Nick Downie
7
+ * Released under the MIT license
8
+ * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md
9
+ */
10
+
11
+
12
+ (function(){
13
+
14
+ "use strict";
15
+
16
+ //Declare root variable - window in the browser, global on the server
17
+ var root = this,
18
+ previous = root.Chart;
19
+
20
+ //Occupy the global variable of Chart, and create a simple base class
21
+ var Chart = function(context){
22
+ var chart = this;
23
+ this.canvas = context.canvas;
24
+
25
+ this.ctx = context;
26
+
27
+ //Variables global to the chart
28
+ var width = this.width = context.canvas.width;
29
+ var height = this.height = context.canvas.height;
30
+ this.aspectRatio = this.width / this.height;
31
+ //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale.
32
+ helpers.retinaScale(this);
33
+
34
+ return this;
35
+ };
36
+ //Globally expose the defaults to allow for user updating/changing
37
+ Chart.defaults = {
38
+ global: {
39
+ // Boolean - Whether to animate the chart
40
+ animation: true,
41
+
42
+ // Number - Number of animation steps
43
+ animationSteps: 60,
44
+
45
+ // String - Animation easing effect
46
+ animationEasing: "easeOutQuart",
47
+
48
+ // Boolean - If we should show the scale at all
49
+ showScale: true,
50
+
51
+ // Boolean - If we want to override with a hard coded scale
52
+ scaleOverride: false,
53
+
54
+ // ** Required if scaleOverride is true **
55
+ // Number - The number of steps in a hard coded scale
56
+ scaleSteps: null,
57
+ // Number - The value jump in the hard coded scale
58
+ scaleStepWidth: null,
59
+ // Number - The scale starting value
60
+ scaleStartValue: null,
61
+
62
+ // String - Colour of the scale line
63
+ scaleLineColor: "rgba(0,0,0,.1)",
64
+
65
+ // Number - Pixel width of the scale line
66
+ scaleLineWidth: 1,
67
+
68
+ // Boolean - Whether to show labels on the scale
69
+ scaleShowLabels: true,
70
+
71
+ // Interpolated JS string - can access value
72
+ scaleLabel: "<%=value%>",
73
+
74
+ // Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there
75
+ scaleIntegersOnly: true,
76
+
77
+ // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
78
+ scaleBeginAtZero: false,
79
+
80
+ // String - Scale label font declaration for the scale label
81
+ scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
82
+
83
+ // Number - Scale label font size in pixels
84
+ scaleFontSize: 12,
85
+
86
+ // String - Scale label font weight style
87
+ scaleFontStyle: "normal",
88
+
89
+ // String - Scale label font colour
90
+ scaleFontColor: "#666",
91
+
92
+ // Boolean - whether or not the chart should be responsive and resize when the browser does.
93
+ responsive: false,
94
+
95
+ // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container
96
+ maintainAspectRatio: true,
97
+
98
+ // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove
99
+ showTooltips: true,
100
+
101
+ // Array - Array of string names to attach tooltip events
102
+ tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"],
103
+
104
+ // String - Tooltip background colour
105
+ tooltipFillColor: "rgba(0,0,0,0.8)",
106
+
107
+ // String - Tooltip label font declaration for the scale label
108
+ tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
109
+
110
+ // Number - Tooltip label font size in pixels
111
+ tooltipFontSize: 14,
112
+
113
+ // String - Tooltip font weight style
114
+ tooltipFontStyle: "normal",
115
+
116
+ // String - Tooltip label font colour
117
+ tooltipFontColor: "#fff",
118
+
119
+ // String - Tooltip title font declaration for the scale label
120
+ tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
121
+
122
+ // Number - Tooltip title font size in pixels
123
+ tooltipTitleFontSize: 14,
124
+
125
+ // String - Tooltip title font weight style
126
+ tooltipTitleFontStyle: "bold",
127
+
128
+ // String - Tooltip title font colour
129
+ tooltipTitleFontColor: "#fff",
130
+
131
+ // Number - pixel width of padding around tooltip text
132
+ tooltipYPadding: 6,
133
+
134
+ // Number - pixel width of padding around tooltip text
135
+ tooltipXPadding: 6,
136
+
137
+ // Number - Size of the caret on the tooltip
138
+ tooltipCaretSize: 8,
139
+
140
+ // Number - Pixel radius of the tooltip border
141
+ tooltipCornerRadius: 6,
142
+
143
+ // Number - Pixel offset from point x to tooltip edge
144
+ tooltipXOffset: 10,
145
+
146
+ // String - Template string for single tooltips
147
+ tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>",
148
+
149
+ // String - Template string for single tooltips
150
+ multiTooltipTemplate: "<%= value %>",
151
+
152
+ // String - Colour behind the legend colour block
153
+ multiTooltipKeyBackground: '#fff',
154
+
155
+ // Function - Will fire on animation progression.
156
+ onAnimationProgress: function(){},
157
+
158
+ // Function - Will fire on animation completion.
159
+ onAnimationComplete: function(){}
160
+
161
+ }
162
+ };
163
+
164
+ //Create a dictionary of chart types, to allow for extension of existing types
165
+ Chart.types = {};
166
+
167
+ //Global Chart helpers object for utility methods and classes
168
+ var helpers = Chart.helpers = {};
169
+
170
+ //-- Basic js utility methods
171
+ var each = helpers.each = function(loopable,callback,self){
172
+ var additionalArgs = Array.prototype.slice.call(arguments, 3);
173
+ // Check to see if null or undefined firstly.
174
+ if (loopable){
175
+ if (loopable.length === +loopable.length){
176
+ var i;
177
+ for (i=0; i<loopable.length; i++){
178
+ callback.apply(self,[loopable[i], i].concat(additionalArgs));
179
+ }
180
+ }
181
+ else{
182
+ for (var item in loopable){
183
+ callback.apply(self,[loopable[item],item].concat(additionalArgs));
184
+ }
185
+ }
186
+ }
187
+ },
188
+ clone = helpers.clone = function(obj){
189
+ var objClone = {};
190
+ each(obj,function(value,key){
191
+ if (obj.hasOwnProperty(key)) objClone[key] = value;
192
+ });
193
+ return objClone;
194
+ },
195
+ extend = helpers.extend = function(base){
196
+ each(Array.prototype.slice.call(arguments,1), function(extensionObject) {
197
+ each(extensionObject,function(value,key){
198
+ if (extensionObject.hasOwnProperty(key)) base[key] = value;
199
+ });
200
+ });
201
+ return base;
202
+ },
203
+ merge = helpers.merge = function(base,master){
204
+ //Merge properties in left object over to a shallow clone of object right.
205
+ var args = Array.prototype.slice.call(arguments,0);
206
+ args.unshift({});
207
+ return extend.apply(null, args);
208
+ },
209
+ indexOf = helpers.indexOf = function(arrayToSearch, item){
210
+ if (Array.prototype.indexOf) {
211
+ return arrayToSearch.indexOf(item);
212
+ }
213
+ else{
214
+ for (var i = 0; i < arrayToSearch.length; i++) {
215
+ if (arrayToSearch[i] === item) return i;
216
+ }
217
+ return -1;
218
+ }
219
+ },
220
+ where = helpers.where = function(collection, filterCallback){
221
+ var filtered = [];
222
+
223
+ helpers.each(collection, function(item){
224
+ if (filterCallback(item)){
225
+ filtered.push(item);
226
+ }
227
+ });
228
+
229
+ return filtered;
230
+ },
231
+ findNextWhere = helpers.findNextWhere = function(arrayToSearch, filterCallback, startIndex){
232
+ // Default to start of the array
233
+ if (!startIndex){
234
+ startIndex = -1;
235
+ }
236
+ for (var i = startIndex + 1; i < arrayToSearch.length; i++) {
237
+ var currentItem = arrayToSearch[i];
238
+ if (filterCallback(currentItem)){
239
+ return currentItem;
240
+ }
241
+ };
242
+ },
243
+ findPreviousWhere = helpers.findPreviousWhere = function(arrayToSearch, filterCallback, startIndex){
244
+ // Default to end of the array
245
+ if (!startIndex){
246
+ startIndex = arrayToSearch.length;
247
+ }
248
+ for (var i = startIndex - 1; i >= 0; i--) {
249
+ var currentItem = arrayToSearch[i];
250
+ if (filterCallback(currentItem)){
251
+ return currentItem;
252
+ }
253
+ };
254
+ },
255
+ inherits = helpers.inherits = function(extensions){
256
+ //Basic javascript inheritance based on the model created in Backbone.js
257
+ var parent = this;
258
+ var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); };
259
+
260
+ var Surrogate = function(){ this.constructor = ChartElement;};
261
+ Surrogate.prototype = parent.prototype;
262
+ ChartElement.prototype = new Surrogate();
263
+
264
+ ChartElement.extend = inherits;
265
+
266
+ if (extensions) extend(ChartElement.prototype, extensions);
267
+
268
+ ChartElement.__super__ = parent.prototype;
269
+
270
+ return ChartElement;
271
+ },
272
+ noop = helpers.noop = function(){},
273
+ uid = helpers.uid = (function(){
274
+ var id=0;
275
+ return function(){
276
+ return "chart-" + id++;
277
+ };
278
+ })(),
279
+ warn = helpers.warn = function(str){
280
+ //Method for warning of errors
281
+ if (window.console && typeof window.console.warn == "function") console.warn(str);
282
+ },
283
+ amd = helpers.amd = (typeof define == 'function' && define.amd),
284
+ //-- Math methods
285
+ isNumber = helpers.isNumber = function(n){
286
+ return !isNaN(parseFloat(n)) && isFinite(n);
287
+ },
288
+ max = helpers.max = function(array){
289
+ return Math.max.apply( Math, array );
290
+ },
291
+ min = helpers.min = function(array){
292
+ return Math.min.apply( Math, array );
293
+ },
294
+ cap = helpers.cap = function(valueToCap,maxValue,minValue){
295
+ if(isNumber(maxValue)) {
296
+ if( valueToCap > maxValue ) {
297
+ return maxValue;
298
+ }
299
+ }
300
+ else if(isNumber(minValue)){
301
+ if ( valueToCap < minValue ){
302
+ return minValue;
303
+ }
304
+ }
305
+ return valueToCap;
306
+ },
307
+ getDecimalPlaces = helpers.getDecimalPlaces = function(num){
308
+ if (num%1!==0 && isNumber(num)){
309
+ return num.toString().split(".")[1].length;
310
+ }
311
+ else {
312
+ return 0;
313
+ }
314
+ },
315
+ toRadians = helpers.radians = function(degrees){
316
+ return degrees * (Math.PI/180);
317
+ },
318
+ // Gets the angle from vertical upright to the point about a centre.
319
+ getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){
320
+ var distanceFromXCenter = anglePoint.x - centrePoint.x,
321
+ distanceFromYCenter = anglePoint.y - centrePoint.y,
322
+ radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter);
323
+
324
+
325
+ var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter);
326
+
327
+ //If the segment is in the top left quadrant, we need to add another rotation to the angle
328
+ if (distanceFromXCenter < 0 && distanceFromYCenter < 0){
329
+ angle += Math.PI*2;
330
+ }
331
+
332
+ return {
333
+ angle: angle,
334
+ distance: radialDistanceFromCenter
335
+ };
336
+ },
337
+ aliasPixel = helpers.aliasPixel = function(pixelWidth){
338
+ return (pixelWidth % 2 === 0) ? 0 : 0.5;
339
+ },
340
+ splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){
341
+ //Props to Rob Spencer at scaled innovation for his post on splining between points
342
+ //http://scaledinnovation.com/analytics/splines/aboutSplines.html
343
+ var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)),
344
+ d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)),
345
+ fa=t*d01/(d01+d12),// scaling factor for triangle Ta
346
+ fb=t*d12/(d01+d12);
347
+ return {
348
+ inner : {
349
+ x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x),
350
+ y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y)
351
+ },
352
+ outer : {
353
+ x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x),
354
+ y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y)
355
+ }
356
+ };
357
+ },
358
+ calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){
359
+ return Math.floor(Math.log(val) / Math.LN10);
360
+ },
361
+ calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){
362
+
363
+ //Set a minimum step of two - a point at the top of the graph, and a point at the base
364
+ var minSteps = 2,
365
+ maxSteps = Math.floor(drawingSize/(textSize * 1.5)),
366
+ skipFitting = (minSteps >= maxSteps);
367
+
368
+ var maxValue = max(valuesArray),
369
+ minValue = min(valuesArray);
370
+
371
+ // We need some degree of seperation here to calculate the scales if all the values are the same
372
+ // Adding/minusing 0.5 will give us a range of 1.
373
+ if (maxValue === minValue){
374
+ maxValue += 0.5;
375
+ // So we don't end up with a graph with a negative start value if we've said always start from zero
376
+ if (minValue >= 0.5 && !startFromZero){
377
+ minValue -= 0.5;
378
+ }
379
+ else{
380
+ // Make up a whole number above the values
381
+ maxValue += 0.5;
382
+ }
383
+ }
384
+
385
+ var valueRange = Math.abs(maxValue - minValue),
386
+ rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange),
387
+ graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude),
388
+ graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude),
389
+ graphRange = graphMax - graphMin,
390
+ stepValue = Math.pow(10, rangeOrderOfMagnitude),
391
+ numberOfSteps = Math.round(graphRange / stepValue);
392
+
393
+ //If we have more space on the graph we'll use it to give more definition to the data
394
+ while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) {
395
+ if(numberOfSteps > maxSteps){
396
+ stepValue *=2;
397
+ numberOfSteps = Math.round(graphRange/stepValue);
398
+ // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps.
399
+ if (numberOfSteps % 1 !== 0){
400
+ skipFitting = true;
401
+ }
402
+ }
403
+ //We can fit in double the amount of scale points on the scale
404
+ else{
405
+ //If user has declared ints only, and the step value isn't a decimal
406
+ if (integersOnly && rangeOrderOfMagnitude >= 0){
407
+ //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float
408
+ if(stepValue/2 % 1 === 0){
409
+ stepValue /=2;
410
+ numberOfSteps = Math.round(graphRange/stepValue);
411
+ }
412
+ //If it would make it a float break out of the loop
413
+ else{
414
+ break;
415
+ }
416
+ }
417
+ //If the scale doesn't have to be an int, make the scale more granular anyway.
418
+ else{
419
+ stepValue /=2;
420
+ numberOfSteps = Math.round(graphRange/stepValue);
421
+ }
422
+
423
+ }
424
+ }
425
+
426
+ if (skipFitting){
427
+ numberOfSteps = minSteps;
428
+ stepValue = graphRange / numberOfSteps;
429
+ }
430
+
431
+ return {
432
+ steps : numberOfSteps,
433
+ stepValue : stepValue,
434
+ min : graphMin,
435
+ max : graphMin + (numberOfSteps * stepValue)
436
+ };
437
+
438
+ },
439
+ /* jshint ignore:start */
440
+ // Blows up jshint errors based on the new Function constructor
441
+ //Templating methods
442
+ //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/
443
+ template = helpers.template = function(templateString, valuesObject){
444
+ // If templateString is function rather than string-template - call the function for valuesObject
445
+ if(templateString instanceof Function){
446
+ return templateString(valuesObject);
447
+ }
448
+
449
+ var cache = {};
450
+ function tmpl(str, data){
451
+ // Figure out if we're getting a template, or if we need to
452
+ // load the template - and be sure to cache the result.
453
+ var fn = !/\W/.test(str) ?
454
+ cache[str] = cache[str] :
455
+
456
+ // Generate a reusable function that will serve as a template
457
+ // generator (and which will be cached).
458
+ new Function("obj",
459
+ "var p=[],print=function(){p.push.apply(p,arguments);};" +
460
+
461
+ // Introduce the data as local variables using with(){}
462
+ "with(obj){p.push('" +
463
+
464
+ // Convert the template into pure JavaScript
465
+ str
466
+ .replace(/[\r\t\n]/g, " ")
467
+ .split("<%").join("\t")
468
+ .replace(/((^|%>)[^\t]*)'/g, "$1\r")
469
+ .replace(/\t=(.*?)%>/g, "',$1,'")
470
+ .split("\t").join("');")
471
+ .split("%>").join("p.push('")
472
+ .split("\r").join("\\'") +
473
+ "');}return p.join('');"
474
+ );
475
+
476
+ // Provide some basic currying to the user
477
+ return data ? fn( data ) : fn;
478
+ }
479
+ return tmpl(templateString,valuesObject);
480
+ },
481
+ /* jshint ignore:end */
482
+ generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){
483
+ var labelsArray = new Array(numberOfSteps);
484
+ if (labelTemplateString){
485
+ each(labelsArray,function(val,index){
486
+ labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))});
487
+ });
488
+ }
489
+ return labelsArray;
490
+ },
491
+ //--Animation methods
492
+ //Easing functions adapted from Robert Penner's easing equations
493
+ //http://www.robertpenner.com/easing/
494
+ easingEffects = helpers.easingEffects = {
495
+ linear: function (t) {
496
+ return t;
497
+ },
498
+ easeInQuad: function (t) {
499
+ return t * t;
500
+ },
501
+ easeOutQuad: function (t) {
502
+ return -1 * t * (t - 2);
503
+ },
504
+ easeInOutQuad: function (t) {
505
+ if ((t /= 1 / 2) < 1) return 1 / 2 * t * t;
506
+ return -1 / 2 * ((--t) * (t - 2) - 1);
507
+ },
508
+ easeInCubic: function (t) {
509
+ return t * t * t;
510
+ },
511
+ easeOutCubic: function (t) {
512
+ return 1 * ((t = t / 1 - 1) * t * t + 1);
513
+ },
514
+ easeInOutCubic: function (t) {
515
+ if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t;
516
+ return 1 / 2 * ((t -= 2) * t * t + 2);
517
+ },
518
+ easeInQuart: function (t) {
519
+ return t * t * t * t;
520
+ },
521
+ easeOutQuart: function (t) {
522
+ return -1 * ((t = t / 1 - 1) * t * t * t - 1);
523
+ },
524
+ easeInOutQuart: function (t) {
525
+ if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t;
526
+ return -1 / 2 * ((t -= 2) * t * t * t - 2);
527
+ },
528
+ easeInQuint: function (t) {
529
+ return 1 * (t /= 1) * t * t * t * t;
530
+ },
531
+ easeOutQuint: function (t) {
532
+ return 1 * ((t = t / 1 - 1) * t * t * t * t + 1);
533
+ },
534
+ easeInOutQuint: function (t) {
535
+ if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t * t;
536
+ return 1 / 2 * ((t -= 2) * t * t * t * t + 2);
537
+ },
538
+ easeInSine: function (t) {
539
+ return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1;
540
+ },
541
+ easeOutSine: function (t) {
542
+ return 1 * Math.sin(t / 1 * (Math.PI / 2));
543
+ },
544
+ easeInOutSine: function (t) {
545
+ return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1);
546
+ },
547
+ easeInExpo: function (t) {
548
+ return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1));
549
+ },
550
+ easeOutExpo: function (t) {
551
+ return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1);
552
+ },
553
+ easeInOutExpo: function (t) {
554
+ if (t === 0) return 0;
555
+ if (t === 1) return 1;
556
+ if ((t /= 1 / 2) < 1) return 1 / 2 * Math.pow(2, 10 * (t - 1));
557
+ return 1 / 2 * (-Math.pow(2, -10 * --t) + 2);
558
+ },
559
+ easeInCirc: function (t) {
560
+ if (t >= 1) return t;
561
+ return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1);
562
+ },
563
+ easeOutCirc: function (t) {
564
+ return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t);
565
+ },
566
+ easeInOutCirc: function (t) {
567
+ if ((t /= 1 / 2) < 1) return -1 / 2 * (Math.sqrt(1 - t * t) - 1);
568
+ return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1);
569
+ },
570
+ easeInElastic: function (t) {
571
+ var s = 1.70158;
572
+ var p = 0;
573
+ var a = 1;
574
+ if (t === 0) return 0;
575
+ if ((t /= 1) == 1) return 1;
576
+ if (!p) p = 1 * 0.3;
577
+ if (a < Math.abs(1)) {
578
+ a = 1;
579
+ s = p / 4;
580
+ } else s = p / (2 * Math.PI) * Math.asin(1 / a);
581
+ return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));
582
+ },
583
+ easeOutElastic: function (t) {
584
+ var s = 1.70158;
585
+ var p = 0;
586
+ var a = 1;
587
+ if (t === 0) return 0;
588
+ if ((t /= 1) == 1) return 1;
589
+ if (!p) p = 1 * 0.3;
590
+ if (a < Math.abs(1)) {
591
+ a = 1;
592
+ s = p / 4;
593
+ } else s = p / (2 * Math.PI) * Math.asin(1 / a);
594
+ return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1;
595
+ },
596
+ easeInOutElastic: function (t) {
597
+ var s = 1.70158;
598
+ var p = 0;
599
+ var a = 1;
600
+ if (t === 0) return 0;
601
+ if ((t /= 1 / 2) == 2) return 1;
602
+ if (!p) p = 1 * (0.3 * 1.5);
603
+ if (a < Math.abs(1)) {
604
+ a = 1;
605
+ s = p / 4;
606
+ } else s = p / (2 * Math.PI) * Math.asin(1 / a);
607
+ if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));
608
+ return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1;
609
+ },
610
+ easeInBack: function (t) {
611
+ var s = 1.70158;
612
+ return 1 * (t /= 1) * t * ((s + 1) * t - s);
613
+ },
614
+ easeOutBack: function (t) {
615
+ var s = 1.70158;
616
+ return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1);
617
+ },
618
+ easeInOutBack: function (t) {
619
+ var s = 1.70158;
620
+ if ((t /= 1 / 2) < 1) return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s));
621
+ return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2);
622
+ },
623
+ easeInBounce: function (t) {
624
+ return 1 - easingEffects.easeOutBounce(1 - t);
625
+ },
626
+ easeOutBounce: function (t) {
627
+ if ((t /= 1) < (1 / 2.75)) {
628
+ return 1 * (7.5625 * t * t);
629
+ } else if (t < (2 / 2.75)) {
630
+ return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75);
631
+ } else if (t < (2.5 / 2.75)) {
632
+ return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375);
633
+ } else {
634
+ return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375);
635
+ }
636
+ },
637
+ easeInOutBounce: function (t) {
638
+ if (t < 1 / 2) return easingEffects.easeInBounce(t * 2) * 0.5;
639
+ return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5;
640
+ }
641
+ },
642
+ //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
643
+ requestAnimFrame = helpers.requestAnimFrame = (function(){
644
+ return window.requestAnimationFrame ||
645
+ window.webkitRequestAnimationFrame ||
646
+ window.mozRequestAnimationFrame ||
647
+ window.oRequestAnimationFrame ||
648
+ window.msRequestAnimationFrame ||
649
+ function(callback) {
650
+ return window.setTimeout(callback, 1000 / 60);
651
+ };
652
+ })(),
653
+ cancelAnimFrame = helpers.cancelAnimFrame = (function(){
654
+ return window.cancelAnimationFrame ||
655
+ window.webkitCancelAnimationFrame ||
656
+ window.mozCancelAnimationFrame ||
657
+ window.oCancelAnimationFrame ||
658
+ window.msCancelAnimationFrame ||
659
+ function(callback) {
660
+ return window.clearTimeout(callback, 1000 / 60);
661
+ };
662
+ })(),
663
+ animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){
664
+
665
+ var currentStep = 0,
666
+ easingFunction = easingEffects[easingString] || easingEffects.linear;
667
+
668
+ var animationFrame = function(){
669
+ currentStep++;
670
+ var stepDecimal = currentStep/totalSteps;
671
+ var easeDecimal = easingFunction(stepDecimal);
672
+
673
+ callback.call(chartInstance,easeDecimal,stepDecimal, currentStep);
674
+ onProgress.call(chartInstance,easeDecimal,stepDecimal);
675
+ if (currentStep < totalSteps){
676
+ chartInstance.animationFrame = requestAnimFrame(animationFrame);
677
+ } else{
678
+ onComplete.apply(chartInstance);
679
+ }
680
+ };
681
+ requestAnimFrame(animationFrame);
682
+ },
683
+ //-- DOM methods
684
+ getRelativePosition = helpers.getRelativePosition = function(evt){
685
+ var mouseX, mouseY;
686
+ var e = evt.originalEvent || evt,
687
+ canvas = evt.currentTarget || evt.srcElement,
688
+ boundingRect = canvas.getBoundingClientRect();
689
+
690
+ if (e.touches){
691
+ mouseX = e.touches[0].clientX - boundingRect.left;
692
+ mouseY = e.touches[0].clientY - boundingRect.top;
693
+
694
+ }
695
+ else{
696
+ mouseX = e.clientX - boundingRect.left;
697
+ mouseY = e.clientY - boundingRect.top;
698
+ }
699
+
700
+ return {
701
+ x : mouseX,
702
+ y : mouseY
703
+ };
704
+
705
+ },
706
+ addEvent = helpers.addEvent = function(node,eventType,method){
707
+ if (node.addEventListener){
708
+ node.addEventListener(eventType,method);
709
+ } else if (node.attachEvent){
710
+ node.attachEvent("on"+eventType, method);
711
+ } else {
712
+ node["on"+eventType] = method;
713
+ }
714
+ },
715
+ removeEvent = helpers.removeEvent = function(node, eventType, handler){
716
+ if (node.removeEventListener){
717
+ node.removeEventListener(eventType, handler, false);
718
+ } else if (node.detachEvent){
719
+ node.detachEvent("on"+eventType,handler);
720
+ } else{
721
+ node["on" + eventType] = noop;
722
+ }
723
+ },
724
+ bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){
725
+ // Create the events object if it's not already present
726
+ if (!chartInstance.events) chartInstance.events = {};
727
+
728
+ each(arrayOfEvents,function(eventName){
729
+ chartInstance.events[eventName] = function(){
730
+ handler.apply(chartInstance, arguments);
731
+ };
732
+ addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]);
733
+ });
734
+ },
735
+ unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) {
736
+ each(arrayOfEvents, function(handler,eventName){
737
+ removeEvent(chartInstance.chart.canvas, eventName, handler);
738
+ });
739
+ },
740
+ getMaximumWidth = helpers.getMaximumWidth = function(domNode){
741
+ var container = domNode.parentNode;
742
+ // TODO = check cross browser stuff with this.
743
+ return container.clientWidth;
744
+ },
745
+ getMaximumHeight = helpers.getMaximumHeight = function(domNode){
746
+ var container = domNode.parentNode;
747
+ // TODO = check cross browser stuff with this.
748
+ return container.clientHeight;
749
+ },
750
+ getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support
751
+ retinaScale = helpers.retinaScale = function(chart){
752
+ var ctx = chart.ctx,
753
+ width = chart.canvas.width,
754
+ height = chart.canvas.height;
755
+
756
+ if (window.devicePixelRatio) {
757
+ ctx.canvas.style.width = width + "px";
758
+ ctx.canvas.style.height = height + "px";
759
+ ctx.canvas.height = height * window.devicePixelRatio;
760
+ ctx.canvas.width = width * window.devicePixelRatio;
761
+ ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
762
+ }
763
+ },
764
+ //-- Canvas methods
765
+ clear = helpers.clear = function(chart){
766
+ chart.ctx.clearRect(0,0,chart.width,chart.height);
767
+ },
768
+ fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){
769
+ return fontStyle + " " + pixelSize+"px " + fontFamily;
770
+ },
771
+ longestText = helpers.longestText = function(ctx,font,arrayOfStrings){
772
+ ctx.font = font;
773
+ var longest = 0;
774
+ each(arrayOfStrings,function(string){
775
+ var textWidth = ctx.measureText(string).width;
776
+ longest = (textWidth > longest) ? textWidth : longest;
777
+ });
778
+ return longest;
779
+ },
780
+ drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){
781
+ ctx.beginPath();
782
+ ctx.moveTo(x + radius, y);
783
+ ctx.lineTo(x + width - radius, y);
784
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
785
+ ctx.lineTo(x + width, y + height - radius);
786
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
787
+ ctx.lineTo(x + radius, y + height);
788
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
789
+ ctx.lineTo(x, y + radius);
790
+ ctx.quadraticCurveTo(x, y, x + radius, y);
791
+ ctx.closePath();
792
+ };
793
+
794
+
795
+ //Store a reference to each instance - allowing us to globally resize chart instances on window resize.
796
+ //Destroy method on the chart will remove the instance of the chart from this reference.
797
+ Chart.instances = {};
798
+
799
+ Chart.Type = function(data,options,chart){
800
+ this.options = options;
801
+ this.chart = chart;
802
+ this.id = uid();
803
+ //Add the chart instance to the global namespace
804
+ Chart.instances[this.id] = this;
805
+
806
+ // Initialize is always called when a chart type is created
807
+ // By default it is a no op, but it should be extended
808
+ if (options.responsive){
809
+ this.resize();
810
+ }
811
+ this.initialize.call(this,data);
812
+ };
813
+
814
+ //Core methods that'll be a part of every chart type
815
+ extend(Chart.Type.prototype,{
816
+ initialize : function(){return this;},
817
+ clear : function(){
818
+ clear(this.chart);
819
+ return this;
820
+ },
821
+ stop : function(){
822
+ // Stops any current animation loop occuring
823
+ helpers.cancelAnimFrame.call(root, this.animationFrame);
824
+ return this;
825
+ },
826
+ resize : function(callback){
827
+ this.stop();
828
+ var canvas = this.chart.canvas,
829
+ newWidth = getMaximumWidth(this.chart.canvas),
830
+ newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas);
831
+
832
+ canvas.width = this.chart.width = newWidth;
833
+ canvas.height = this.chart.height = newHeight;
834
+
835
+ retinaScale(this.chart);
836
+
837
+ if (typeof callback === "function"){
838
+ callback.apply(this, Array.prototype.slice.call(arguments, 1));
839
+ }
840
+ return this;
841
+ },
842
+ reflow : noop,
843
+ render : function(reflow){
844
+ if (reflow){
845
+ this.reflow();
846
+ }
847
+ if (this.options.animation && !reflow){
848
+ helpers.animationLoop(
849
+ this.draw,
850
+ this.options.animationSteps,
851
+ this.options.animationEasing,
852
+ this.options.onAnimationProgress,
853
+ this.options.onAnimationComplete,
854
+ this
855
+ );
856
+ }
857
+ else{
858
+ this.draw();
859
+ this.options.onAnimationComplete.call(this);
860
+ }
861
+ return this;
862
+ },
863
+ generateLegend : function(){
864
+ return template(this.options.legendTemplate,this);
865
+ },
866
+ destroy : function(){
867
+ this.clear();
868
+ unbindEvents(this, this.events);
869
+ delete Chart.instances[this.id];
870
+ },
871
+ showTooltip : function(ChartElements, forceRedraw){
872
+ // Only redraw the chart if we've actually changed what we're hovering on.
873
+ if (typeof this.activeElements === 'undefined') this.activeElements = [];
874
+
875
+ var isChanged = (function(Elements){
876
+ var changed = false;
877
+
878
+ if (Elements.length !== this.activeElements.length){
879
+ changed = true;
880
+ return changed;
881
+ }
882
+
883
+ each(Elements, function(element, index){
884
+ if (element !== this.activeElements[index]){
885
+ changed = true;
886
+ }
887
+ }, this);
888
+ return changed;
889
+ }).call(this, ChartElements);
890
+
891
+ if (!isChanged && !forceRedraw){
892
+ return;
893
+ }
894
+ else{
895
+ this.activeElements = ChartElements;
896
+ }
897
+ this.draw();
898
+ if (ChartElements.length > 0){
899
+ // If we have multiple datasets, show a MultiTooltip for all of the data points at that index
900
+ if (this.datasets && this.datasets.length > 1) {
901
+ var dataArray,
902
+ dataIndex;
903
+
904
+ for (var i = this.datasets.length - 1; i >= 0; i--) {
905
+ dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments;
906
+ dataIndex = indexOf(dataArray, ChartElements[0]);
907
+ if (dataIndex !== -1){
908
+ break;
909
+ }
910
+ }
911
+ var tooltipLabels = [],
912
+ tooltipColors = [],
913
+ medianPosition = (function(index) {
914
+
915
+ // Get all the points at that particular index
916
+ var Elements = [],
917
+ dataCollection,
918
+ xPositions = [],
919
+ yPositions = [],
920
+ xMax,
921
+ yMax,
922
+ xMin,
923
+ yMin;
924
+ helpers.each(this.datasets, function(dataset){
925
+ dataCollection = dataset.points || dataset.bars || dataset.segments;
926
+ if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){
927
+ Elements.push(dataCollection[dataIndex]);
928
+ }
929
+ });
930
+
931
+ helpers.each(Elements, function(element) {
932
+ xPositions.push(element.x);
933
+ yPositions.push(element.y);
934
+
935
+
936
+ //Include any colour information about the element
937
+ tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element));
938
+ tooltipColors.push({
939
+ fill: element._saved.fillColor || element.fillColor,
940
+ stroke: element._saved.strokeColor || element.strokeColor
941
+ });
942
+
943
+ }, this);
944
+
945
+ yMin = min(yPositions);
946
+ yMax = max(yPositions);
947
+
948
+ xMin = min(xPositions);
949
+ xMax = max(xPositions);
950
+
951
+ return {
952
+ x: (xMin > this.chart.width/2) ? xMin : xMax,
953
+ y: (yMin + yMax)/2
954
+ };
955
+ }).call(this, dataIndex);
956
+
957
+ new Chart.MultiTooltip({
958
+ x: medianPosition.x,
959
+ y: medianPosition.y,
960
+ xPadding: this.options.tooltipXPadding,
961
+ yPadding: this.options.tooltipYPadding,
962
+ xOffset: this.options.tooltipXOffset,
963
+ fillColor: this.options.tooltipFillColor,
964
+ textColor: this.options.tooltipFontColor,
965
+ fontFamily: this.options.tooltipFontFamily,
966
+ fontStyle: this.options.tooltipFontStyle,
967
+ fontSize: this.options.tooltipFontSize,
968
+ titleTextColor: this.options.tooltipTitleFontColor,
969
+ titleFontFamily: this.options.tooltipTitleFontFamily,
970
+ titleFontStyle: this.options.tooltipTitleFontStyle,
971
+ titleFontSize: this.options.tooltipTitleFontSize,
972
+ cornerRadius: this.options.tooltipCornerRadius,
973
+ labels: tooltipLabels,
974
+ legendColors: tooltipColors,
975
+ legendColorBackground : this.options.multiTooltipKeyBackground,
976
+ title: ChartElements[0].label,
977
+ chart: this.chart,
978
+ ctx: this.chart.ctx
979
+ }).draw();
980
+
981
+ } else {
982
+ each(ChartElements, function(Element) {
983
+ var tooltipPosition = Element.tooltipPosition();
984
+ new Chart.Tooltip({
985
+ x: Math.round(tooltipPosition.x),
986
+ y: Math.round(tooltipPosition.y),
987
+ xPadding: this.options.tooltipXPadding,
988
+ yPadding: this.options.tooltipYPadding,
989
+ fillColor: this.options.tooltipFillColor,
990
+ textColor: this.options.tooltipFontColor,
991
+ fontFamily: this.options.tooltipFontFamily,
992
+ fontStyle: this.options.tooltipFontStyle,
993
+ fontSize: this.options.tooltipFontSize,
994
+ caretHeight: this.options.tooltipCaretSize,
995
+ cornerRadius: this.options.tooltipCornerRadius,
996
+ text: template(this.options.tooltipTemplate, Element),
997
+ chart: this.chart
998
+ }).draw();
999
+ }, this);
1000
+ }
1001
+ }
1002
+ return this;
1003
+ },
1004
+ toBase64Image : function(){
1005
+ return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments);
1006
+ }
1007
+ });
1008
+
1009
+ Chart.Type.extend = function(extensions){
1010
+
1011
+ var parent = this;
1012
+
1013
+ var ChartType = function(){
1014
+ return parent.apply(this,arguments);
1015
+ };
1016
+
1017
+ //Copy the prototype object of the this class
1018
+ ChartType.prototype = clone(parent.prototype);
1019
+ //Now overwrite some of the properties in the base class with the new extensions
1020
+ extend(ChartType.prototype, extensions);
1021
+
1022
+ ChartType.extend = Chart.Type.extend;
1023
+
1024
+ if (extensions.name || parent.prototype.name){
1025
+
1026
+ var chartName = extensions.name || parent.prototype.name;
1027
+ //Assign any potential default values of the new chart type
1028
+
1029
+ //If none are defined, we'll use a clone of the chart type this is being extended from.
1030
+ //I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart
1031
+ //doesn't define some defaults of their own.
1032
+
1033
+ var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {};
1034
+
1035
+ Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults);
1036
+
1037
+ Chart.types[chartName] = ChartType;
1038
+
1039
+ //Register this new chart type in the Chart prototype
1040
+ Chart.prototype[chartName] = function(data,options){
1041
+ var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {});
1042
+ return new ChartType(data,config,this);
1043
+ };
1044
+ } else{
1045
+ warn("Name not provided for this chart, so it hasn't been registered");
1046
+ }
1047
+ return parent;
1048
+ };
1049
+
1050
+ Chart.Element = function(configuration){
1051
+ extend(this,configuration);
1052
+ this.initialize.apply(this,arguments);
1053
+ this.save();
1054
+ };
1055
+ extend(Chart.Element.prototype,{
1056
+ initialize : function(){},
1057
+ restore : function(props){
1058
+ if (!props){
1059
+ extend(this,this._saved);
1060
+ } else {
1061
+ each(props,function(key){
1062
+ this[key] = this._saved[key];
1063
+ },this);
1064
+ }
1065
+ return this;
1066
+ },
1067
+ save : function(){
1068
+ this._saved = clone(this);
1069
+ delete this._saved._saved;
1070
+ return this;
1071
+ },
1072
+ update : function(newProps){
1073
+ each(newProps,function(value,key){
1074
+ this._saved[key] = this[key];
1075
+ this[key] = value;
1076
+ },this);
1077
+ return this;
1078
+ },
1079
+ transition : function(props,ease){
1080
+ each(props,function(value,key){
1081
+ this[key] = ((value - this._saved[key]) * ease) + this._saved[key];
1082
+ },this);
1083
+ return this;
1084
+ },
1085
+ tooltipPosition : function(){
1086
+ return {
1087
+ x : this.x,
1088
+ y : this.y
1089
+ };
1090
+ },
1091
+ hasValue: function(){
1092
+ return isNumber(this.value);
1093
+ }
1094
+ });
1095
+
1096
+ Chart.Element.extend = inherits;
1097
+
1098
+
1099
+ Chart.Point = Chart.Element.extend({
1100
+ display: true,
1101
+ inRange: function(chartX,chartY){
1102
+ var hitDetectionRange = this.hitDetectionRadius + this.radius;
1103
+ return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2));
1104
+ },
1105
+ draw : function(){
1106
+ if (this.display){
1107
+ var ctx = this.ctx;
1108
+ ctx.beginPath();
1109
+
1110
+ ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2);
1111
+ ctx.closePath();
1112
+
1113
+ ctx.strokeStyle = this.strokeColor;
1114
+ ctx.lineWidth = this.strokeWidth;
1115
+
1116
+ ctx.fillStyle = this.fillColor;
1117
+
1118
+ ctx.fill();
1119
+ ctx.stroke();
1120
+ }
1121
+
1122
+
1123
+ //Quick debug for bezier curve splining
1124
+ //Highlights control points and the line between them.
1125
+ //Handy for dev - stripped in the min version.
1126
+
1127
+ // ctx.save();
1128
+ // ctx.fillStyle = "black";
1129
+ // ctx.strokeStyle = "black"
1130
+ // ctx.beginPath();
1131
+ // ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2);
1132
+ // ctx.fill();
1133
+
1134
+ // ctx.beginPath();
1135
+ // ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2);
1136
+ // ctx.fill();
1137
+
1138
+ // ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y);
1139
+ // ctx.lineTo(this.x, this.y);
1140
+ // ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y);
1141
+ // ctx.stroke();
1142
+
1143
+ // ctx.restore();
1144
+
1145
+
1146
+
1147
+ }
1148
+ });
1149
+
1150
+ Chart.Arc = Chart.Element.extend({
1151
+ inRange : function(chartX,chartY){
1152
+
1153
+ var pointRelativePosition = helpers.getAngleFromPoint(this, {
1154
+ x: chartX,
1155
+ y: chartY
1156
+ });
1157
+
1158
+ //Check if within the range of the open/close angle
1159
+ var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle),
1160
+ withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius);
1161
+
1162
+ return (betweenAngles && withinRadius);
1163
+ //Ensure within the outside of the arc centre, but inside arc outer
1164
+ },
1165
+ tooltipPosition : function(){
1166
+ var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2),
1167
+ rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius;
1168
+ return {
1169
+ x : this.x + (Math.cos(centreAngle) * rangeFromCentre),
1170
+ y : this.y + (Math.sin(centreAngle) * rangeFromCentre)
1171
+ };
1172
+ },
1173
+ draw : function(animationPercent){
1174
+
1175
+ var easingDecimal = animationPercent || 1;
1176
+
1177
+ var ctx = this.ctx;
1178
+
1179
+ ctx.beginPath();
1180
+
1181
+ ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle);
1182
+
1183
+ ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true);
1184
+
1185
+ ctx.closePath();
1186
+ ctx.strokeStyle = this.strokeColor;
1187
+ ctx.lineWidth = this.strokeWidth;
1188
+
1189
+ ctx.fillStyle = this.fillColor;
1190
+
1191
+ ctx.fill();
1192
+ ctx.lineJoin = 'bevel';
1193
+
1194
+ if (this.showStroke){
1195
+ ctx.stroke();
1196
+ }
1197
+ }
1198
+ });
1199
+
1200
+ Chart.Rectangle = Chart.Element.extend({
1201
+ draw : function(){
1202
+ var ctx = this.ctx,
1203
+ halfWidth = this.width/2,
1204
+ leftX = this.x - halfWidth,
1205
+ rightX = this.x + halfWidth,
1206
+ top = this.base - (this.base - this.y),
1207
+ halfStroke = this.strokeWidth / 2;
1208
+
1209
+ // Canvas doesn't allow us to stroke inside the width so we can
1210
+ // adjust the sizes to fit if we're setting a stroke on the line
1211
+ if (this.showStroke){
1212
+ leftX += halfStroke;
1213
+ rightX -= halfStroke;
1214
+ top += halfStroke;
1215
+ }
1216
+
1217
+ ctx.beginPath();
1218
+
1219
+ ctx.fillStyle = this.fillColor;
1220
+ ctx.strokeStyle = this.strokeColor;
1221
+ ctx.lineWidth = this.strokeWidth;
1222
+
1223
+ // It'd be nice to keep this class totally generic to any rectangle
1224
+ // and simply specify which border to miss out.
1225
+ ctx.moveTo(leftX, this.base);
1226
+ ctx.lineTo(leftX, top);
1227
+ ctx.lineTo(rightX, top);
1228
+ ctx.lineTo(rightX, this.base);
1229
+ ctx.fill();
1230
+ if (this.showStroke){
1231
+ ctx.stroke();
1232
+ }
1233
+ },
1234
+ height : function(){
1235
+ return this.base - this.y;
1236
+ },
1237
+ inRange : function(chartX,chartY){
1238
+ return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base);
1239
+ }
1240
+ });
1241
+
1242
+ Chart.Tooltip = Chart.Element.extend({
1243
+ draw : function(){
1244
+
1245
+ var ctx = this.chart.ctx;
1246
+
1247
+ ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily);
1248
+
1249
+ this.xAlign = "center";
1250
+ this.yAlign = "above";
1251
+
1252
+ //Distance between the actual element.y position and the start of the tooltip caret
1253
+ var caretPadding = 2;
1254
+
1255
+ var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding,
1256
+ tooltipRectHeight = this.fontSize + 2*this.yPadding,
1257
+ tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding;
1258
+
1259
+ if (this.x + tooltipWidth/2 >this.chart.width){
1260
+ this.xAlign = "left";
1261
+ } else if (this.x - tooltipWidth/2 < 0){
1262
+ this.xAlign = "right";
1263
+ }
1264
+
1265
+ if (this.y - tooltipHeight < 0){
1266
+ this.yAlign = "below";
1267
+ }
1268
+
1269
+
1270
+ var tooltipX = this.x - tooltipWidth/2,
1271
+ tooltipY = this.y - tooltipHeight;
1272
+
1273
+ ctx.fillStyle = this.fillColor;
1274
+
1275
+ switch(this.yAlign)
1276
+ {
1277
+ case "above":
1278
+ //Draw a caret above the x/y
1279
+ ctx.beginPath();
1280
+ ctx.moveTo(this.x,this.y - caretPadding);
1281
+ ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight));
1282
+ ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight));
1283
+ ctx.closePath();
1284
+ ctx.fill();
1285
+ break;
1286
+ case "below":
1287
+ tooltipY = this.y + caretPadding + this.caretHeight;
1288
+ //Draw a caret below the x/y
1289
+ ctx.beginPath();
1290
+ ctx.moveTo(this.x, this.y + caretPadding);
1291
+ ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight);
1292
+ ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight);
1293
+ ctx.closePath();
1294
+ ctx.fill();
1295
+ break;
1296
+ }
1297
+
1298
+ switch(this.xAlign)
1299
+ {
1300
+ case "left":
1301
+ tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight);
1302
+ break;
1303
+ case "right":
1304
+ tooltipX = this.x - (this.cornerRadius + this.caretHeight);
1305
+ break;
1306
+ }
1307
+
1308
+ drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius);
1309
+
1310
+ ctx.fill();
1311
+
1312
+ ctx.fillStyle = this.textColor;
1313
+ ctx.textAlign = "center";
1314
+ ctx.textBaseline = "middle";
1315
+ ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2);
1316
+ }
1317
+ });
1318
+
1319
+ Chart.MultiTooltip = Chart.Element.extend({
1320
+ initialize : function(){
1321
+ this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily);
1322
+
1323
+ this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily);
1324
+
1325
+ this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5;
1326
+
1327
+ this.ctx.font = this.titleFont;
1328
+
1329
+ var titleWidth = this.ctx.measureText(this.title).width,
1330
+ //Label has a legend square as well so account for this.
1331
+ labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3,
1332
+ longestTextWidth = max([labelWidth,titleWidth]);
1333
+
1334
+ this.width = longestTextWidth + (this.xPadding*2);
1335
+
1336
+
1337
+ var halfHeight = this.height/2;
1338
+
1339
+ //Check to ensure the height will fit on the canvas
1340
+ //The three is to buffer form the very
1341
+ if (this.y - halfHeight < 0 ){
1342
+ this.y = halfHeight;
1343
+ } else if (this.y + halfHeight > this.chart.height){
1344
+ this.y = this.chart.height - halfHeight;
1345
+ }
1346
+
1347
+ //Decide whether to align left or right based on position on canvas
1348
+ if (this.x > this.chart.width/2){
1349
+ this.x -= this.xOffset + this.width;
1350
+ } else {
1351
+ this.x += this.xOffset;
1352
+ }
1353
+
1354
+
1355
+ },
1356
+ getLineHeight : function(index){
1357
+ var baseLineHeight = this.y - (this.height/2) + this.yPadding,
1358
+ afterTitleIndex = index-1;
1359
+
1360
+ //If the index is zero, we're getting the title
1361
+ if (index === 0){
1362
+ return baseLineHeight + this.titleFontSize/2;
1363
+ } else{
1364
+ return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5;
1365
+ }
1366
+
1367
+ },
1368
+ draw : function(){
1369
+ drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius);
1370
+ var ctx = this.ctx;
1371
+ ctx.fillStyle = this.fillColor;
1372
+ ctx.fill();
1373
+ ctx.closePath();
1374
+
1375
+ ctx.textAlign = "left";
1376
+ ctx.textBaseline = "middle";
1377
+ ctx.fillStyle = this.titleTextColor;
1378
+ ctx.font = this.titleFont;
1379
+
1380
+ ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0));
1381
+
1382
+ ctx.font = this.font;
1383
+ helpers.each(this.labels,function(label,index){
1384
+ ctx.fillStyle = this.textColor;
1385
+ ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1));
1386
+
1387
+ //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas)
1388
+ //ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
1389
+ //Instead we'll make a white filled block to put the legendColour palette over.
1390
+
1391
+ ctx.fillStyle = this.legendColorBackground;
1392
+ ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
1393
+
1394
+ ctx.fillStyle = this.legendColors[index].fill;
1395
+ ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
1396
+
1397
+
1398
+ },this);
1399
+ }
1400
+ });
1401
+
1402
+ Chart.Scale = Chart.Element.extend({
1403
+ initialize : function(){
1404
+ this.fit();
1405
+ },
1406
+ buildYLabels : function(){
1407
+ this.yLabels = [];
1408
+
1409
+ var stepDecimalPlaces = getDecimalPlaces(this.stepValue);
1410
+
1411
+ for (var i=0; i<=this.steps; i++){
1412
+ this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)}));
1413
+ }
1414
+ this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) : 0;
1415
+ },
1416
+ addXLabel : function(label){
1417
+ this.xLabels.push(label);
1418
+ this.valuesCount++;
1419
+ this.fit();
1420
+ },
1421
+ removeXLabel : function(){
1422
+ this.xLabels.shift();
1423
+ this.valuesCount--;
1424
+ this.fit();
1425
+ },
1426
+ // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use
1427
+ fit: function(){
1428
+ // First we need the width of the yLabels, assuming the xLabels aren't rotated
1429
+
1430
+ // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation
1431
+ this.startPoint = (this.display) ? this.fontSize : 0;
1432
+ this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels
1433
+
1434
+ // Apply padding settings to the start and end point.
1435
+ this.startPoint += this.padding;
1436
+ this.endPoint -= this.padding;
1437
+
1438
+ // Cache the starting height, so can determine if we need to recalculate the scale yAxis
1439
+ var cachedHeight = this.endPoint - this.startPoint,
1440
+ cachedYLabelWidth;
1441
+
1442
+ // Build the current yLabels so we have an idea of what size they'll be to start
1443
+ /*
1444
+ * This sets what is returned from calculateScaleRange as static properties of this class:
1445
+ *
1446
+ this.steps;
1447
+ this.stepValue;
1448
+ this.min;
1449
+ this.max;
1450
+ *
1451
+ */
1452
+ this.calculateYRange(cachedHeight);
1453
+
1454
+ // With these properties set we can now build the array of yLabels
1455
+ // and also the width of the largest yLabel
1456
+ this.buildYLabels();
1457
+
1458
+ this.calculateXLabelRotation();
1459
+
1460
+ while((cachedHeight > this.endPoint - this.startPoint)){
1461
+ cachedHeight = this.endPoint - this.startPoint;
1462
+ cachedYLabelWidth = this.yLabelWidth;
1463
+
1464
+ this.calculateYRange(cachedHeight);
1465
+ this.buildYLabels();
1466
+
1467
+ // Only go through the xLabel loop again if the yLabel width has changed
1468
+ if (cachedYLabelWidth < this.yLabelWidth){
1469
+ this.calculateXLabelRotation();
1470
+ }
1471
+ }
1472
+
1473
+ },
1474
+ calculateXLabelRotation : function(){
1475
+ //Get the width of each grid by calculating the difference
1476
+ //between x offsets between 0 and 1.
1477
+
1478
+ this.ctx.font = this.font;
1479
+
1480
+ var firstWidth = this.ctx.measureText(this.xLabels[0]).width,
1481
+ lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width,
1482
+ firstRotated,
1483
+ lastRotated;
1484
+
1485
+
1486
+ this.xScalePaddingRight = lastWidth/2 + 3;
1487
+ this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10;
1488
+
1489
+ this.xLabelRotation = 0;
1490
+ if (this.display){
1491
+ var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels),
1492
+ cosRotation,
1493
+ firstRotatedWidth;
1494
+ this.xLabelWidth = originalLabelWidth;
1495
+ //Allow 3 pixels x2 padding either side for label readability
1496
+ var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6;
1497
+
1498
+ //Max label rotate should be 90 - also act as a loop counter
1499
+ while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){
1500
+ cosRotation = Math.cos(toRadians(this.xLabelRotation));
1501
+
1502
+ firstRotated = cosRotation * firstWidth;
1503
+ lastRotated = cosRotation * lastWidth;
1504
+
1505
+ // We're right aligning the text now.
1506
+ if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){
1507
+ this.xScalePaddingLeft = firstRotated + this.fontSize / 2;
1508
+ }
1509
+ this.xScalePaddingRight = this.fontSize/2;
1510
+
1511
+
1512
+ this.xLabelRotation++;
1513
+ this.xLabelWidth = cosRotation * originalLabelWidth;
1514
+
1515
+ }
1516
+ if (this.xLabelRotation > 0){
1517
+ this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3;
1518
+ }
1519
+ }
1520
+ else{
1521
+ this.xLabelWidth = 0;
1522
+ this.xScalePaddingRight = this.padding;
1523
+ this.xScalePaddingLeft = this.padding;
1524
+ }
1525
+
1526
+ },
1527
+ // Needs to be overidden in each Chart type
1528
+ // Otherwise we need to pass all the data into the scale class
1529
+ calculateYRange: noop,
1530
+ drawingArea: function(){
1531
+ return this.startPoint - this.endPoint;
1532
+ },
1533
+ calculateY : function(value){
1534
+ var scalingFactor = this.drawingArea() / (this.min - this.max);
1535
+ return this.endPoint - (scalingFactor * (value - this.min));
1536
+ },
1537
+ calculateX : function(index){
1538
+ var isRotated = (this.xLabelRotation > 0),
1539
+ // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding,
1540
+ innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight),
1541
+ valueWidth = innerWidth/(this.valuesCount - ((this.offsetGridLines) ? 0 : 1)),
1542
+ valueOffset = (valueWidth * index) + this.xScalePaddingLeft;
1543
+
1544
+ if (this.offsetGridLines){
1545
+ valueOffset += (valueWidth/2);
1546
+ }
1547
+
1548
+ return Math.round(valueOffset);
1549
+ },
1550
+ update : function(newProps){
1551
+ helpers.extend(this, newProps);
1552
+ this.fit();
1553
+ },
1554
+ draw : function(){
1555
+ var ctx = this.ctx,
1556
+ yLabelGap = (this.endPoint - this.startPoint) / this.steps,
1557
+ xStart = Math.round(this.xScalePaddingLeft);
1558
+ if (this.display){
1559
+ ctx.fillStyle = this.textColor;
1560
+ ctx.font = this.font;
1561
+ each(this.yLabels,function(labelString,index){
1562
+ var yLabelCenter = this.endPoint - (yLabelGap * index),
1563
+ linePositionY = Math.round(yLabelCenter);
1564
+
1565
+ ctx.textAlign = "right";
1566
+ ctx.textBaseline = "middle";
1567
+ if (this.showLabels){
1568
+ ctx.fillText(labelString,xStart - 10,yLabelCenter);
1569
+ }
1570
+ ctx.beginPath();
1571
+ if (index > 0){
1572
+ // This is a grid line in the centre, so drop that
1573
+ ctx.lineWidth = this.gridLineWidth;
1574
+ ctx.strokeStyle = this.gridLineColor;
1575
+ } else {
1576
+ // This is the first line on the scale
1577
+ ctx.lineWidth = this.lineWidth;
1578
+ ctx.strokeStyle = this.lineColor;
1579
+ }
1580
+
1581
+ linePositionY += helpers.aliasPixel(ctx.lineWidth);
1582
+
1583
+ ctx.moveTo(xStart, linePositionY);
1584
+ ctx.lineTo(this.width, linePositionY);
1585
+ ctx.stroke();
1586
+ ctx.closePath();
1587
+
1588
+ ctx.lineWidth = this.lineWidth;
1589
+ ctx.strokeStyle = this.lineColor;
1590
+ ctx.beginPath();
1591
+ ctx.moveTo(xStart - 5, linePositionY);
1592
+ ctx.lineTo(xStart, linePositionY);
1593
+ ctx.stroke();
1594
+ ctx.closePath();
1595
+
1596
+ },this);
1597
+
1598
+ each(this.xLabels,function(label,index){
1599
+ var xPos = this.calculateX(index) + aliasPixel(this.lineWidth),
1600
+ // Check to see if line/bar here and decide where to place the line
1601
+ linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth),
1602
+ isRotated = (this.xLabelRotation > 0);
1603
+
1604
+ ctx.beginPath();
1605
+
1606
+ if (index > 0){
1607
+ // This is a grid line in the centre, so drop that
1608
+ ctx.lineWidth = this.gridLineWidth;
1609
+ ctx.strokeStyle = this.gridLineColor;
1610
+ } else {
1611
+ // This is the first line on the scale
1612
+ ctx.lineWidth = this.lineWidth;
1613
+ ctx.strokeStyle = this.lineColor;
1614
+ }
1615
+ ctx.moveTo(linePos,this.endPoint);
1616
+ ctx.lineTo(linePos,this.startPoint - 3);
1617
+ ctx.stroke();
1618
+ ctx.closePath();
1619
+
1620
+
1621
+ ctx.lineWidth = this.lineWidth;
1622
+ ctx.strokeStyle = this.lineColor;
1623
+
1624
+
1625
+ // Small lines at the bottom of the base grid line
1626
+ ctx.beginPath();
1627
+ ctx.moveTo(linePos,this.endPoint);
1628
+ ctx.lineTo(linePos,this.endPoint + 5);
1629
+ ctx.stroke();
1630
+ ctx.closePath();
1631
+
1632
+ ctx.save();
1633
+ ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8);
1634
+ ctx.rotate(toRadians(this.xLabelRotation)*-1);
1635
+ ctx.font = this.font;
1636
+ ctx.textAlign = (isRotated) ? "right" : "center";
1637
+ ctx.textBaseline = (isRotated) ? "middle" : "top";
1638
+ ctx.fillText(label, 0, 0);
1639
+ ctx.restore();
1640
+ },this);
1641
+
1642
+ }
1643
+ }
1644
+
1645
+ });
1646
+
1647
+ Chart.RadialScale = Chart.Element.extend({
1648
+ initialize: function(){
1649
+ this.size = min([this.height, this.width]);
1650
+ this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2);
1651
+ },
1652
+ calculateCenterOffset: function(value){
1653
+ // Take into account half font size + the yPadding of the top value
1654
+ var scalingFactor = this.drawingArea / (this.max - this.min);
1655
+
1656
+ return (value - this.min) * scalingFactor;
1657
+ },
1658
+ update : function(){
1659
+ if (!this.lineArc){
1660
+ this.setScaleSize();
1661
+ } else {
1662
+ this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2);
1663
+ }
1664
+ this.buildYLabels();
1665
+ },
1666
+ buildYLabels: function(){
1667
+ this.yLabels = [];
1668
+
1669
+ var stepDecimalPlaces = getDecimalPlaces(this.stepValue);
1670
+
1671
+ for (var i=0; i<=this.steps; i++){
1672
+ this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)}));
1673
+ }
1674
+ },
1675
+ getCircumference : function(){
1676
+ return ((Math.PI*2) / this.valuesCount);
1677
+ },
1678
+ setScaleSize: function(){
1679
+ /*
1680
+ * Right, this is really confusing and there is a lot of maths going on here
1681
+ * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9
1682
+ *
1683
+ * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif
1684
+ *
1685
+ * Solution:
1686
+ *
1687
+ * We assume the radius of the polygon is half the size of the canvas at first
1688
+ * at each index we check if the text overlaps.
1689
+ *
1690
+ * Where it does, we store that angle and that index.
1691
+ *
1692
+ * After finding the largest index and angle we calculate how much we need to remove
1693
+ * from the shape radius to move the point inwards by that x.
1694
+ *
1695
+ * We average the left and right distances to get the maximum shape radius that can fit in the box
1696
+ * along with labels.
1697
+ *
1698
+ * Once we have that, we can find the centre point for the chart, by taking the x text protrusion
1699
+ * on each side, removing that from the size, halving it and adding the left x protrusion width.
1700
+ *
1701
+ * This will mean we have a shape fitted to the canvas, as large as it can be with the labels
1702
+ * and position it in the most space efficient manner
1703
+ *
1704
+ * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif
1705
+ */
1706
+
1707
+
1708
+ // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width.
1709
+ // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points
1710
+ var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]),
1711
+ pointPosition,
1712
+ i,
1713
+ textWidth,
1714
+ halfTextWidth,
1715
+ furthestRight = this.width,
1716
+ furthestRightIndex,
1717
+ furthestRightAngle,
1718
+ furthestLeft = 0,
1719
+ furthestLeftIndex,
1720
+ furthestLeftAngle,
1721
+ xProtrusionLeft,
1722
+ xProtrusionRight,
1723
+ radiusReductionRight,
1724
+ radiusReductionLeft,
1725
+ maxWidthRadius;
1726
+ this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily);
1727
+ for (i=0;i<this.valuesCount;i++){
1728
+ // 5px to space the text slightly out - similar to what we do in the draw function.
1729
+ pointPosition = this.getPointPosition(i, largestPossibleRadius);
1730
+ textWidth = this.ctx.measureText(template(this.templateString, { value: this.labels[i] })).width + 5;
1731
+ if (i === 0 || i === this.valuesCount/2){
1732
+ // If we're at index zero, or exactly the middle, we're at exactly the top/bottom
1733
+ // of the radar chart, so text will be aligned centrally, so we'll half it and compare
1734
+ // w/left and right text sizes
1735
+ halfTextWidth = textWidth/2;
1736
+ if (pointPosition.x + halfTextWidth > furthestRight) {
1737
+ furthestRight = pointPosition.x + halfTextWidth;
1738
+ furthestRightIndex = i;
1739
+ }
1740
+ if (pointPosition.x - halfTextWidth < furthestLeft) {
1741
+ furthestLeft = pointPosition.x - halfTextWidth;
1742
+ furthestLeftIndex = i;
1743
+ }
1744
+ }
1745
+ else if (i < this.valuesCount/2) {
1746
+ // Less than half the values means we'll left align the text
1747
+ if (pointPosition.x + textWidth > furthestRight) {
1748
+ furthestRight = pointPosition.x + textWidth;
1749
+ furthestRightIndex = i;
1750
+ }
1751
+ }
1752
+ else if (i > this.valuesCount/2){
1753
+ // More than half the values means we'll right align the text
1754
+ if (pointPosition.x - textWidth < furthestLeft) {
1755
+ furthestLeft = pointPosition.x - textWidth;
1756
+ furthestLeftIndex = i;
1757
+ }
1758
+ }
1759
+ }
1760
+
1761
+ xProtrusionLeft = furthestLeft;
1762
+
1763
+ xProtrusionRight = Math.ceil(furthestRight - this.width);
1764
+
1765
+ furthestRightAngle = this.getIndexAngle(furthestRightIndex);
1766
+
1767
+ furthestLeftAngle = this.getIndexAngle(furthestLeftIndex);
1768
+
1769
+ radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2);
1770
+
1771
+ radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2);
1772
+
1773
+ // Ensure we actually need to reduce the size of the chart
1774
+ radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0;
1775
+ radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0;
1776
+
1777
+ this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2;
1778
+
1779
+ //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2])
1780
+ this.setCenterPoint(radiusReductionLeft, radiusReductionRight);
1781
+
1782
+ },
1783
+ setCenterPoint: function(leftMovement, rightMovement){
1784
+
1785
+ var maxRight = this.width - rightMovement - this.drawingArea,
1786
+ maxLeft = leftMovement + this.drawingArea;
1787
+
1788
+ this.xCenter = (maxLeft + maxRight)/2;
1789
+ // Always vertically in the centre as the text height doesn't change
1790
+ this.yCenter = (this.height/2);
1791
+ },
1792
+
1793
+ getIndexAngle : function(index){
1794
+ var angleMultiplier = (Math.PI * 2) / this.valuesCount;
1795
+ // Start from the top instead of right, so remove a quarter of the circle
1796
+
1797
+ return index * angleMultiplier - (Math.PI/2);
1798
+ },
1799
+ getPointPosition : function(index, distanceFromCenter){
1800
+ var thisAngle = this.getIndexAngle(index);
1801
+ return {
1802
+ x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter,
1803
+ y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter
1804
+ };
1805
+ },
1806
+ draw: function(){
1807
+ if (this.display){
1808
+ var ctx = this.ctx;
1809
+ each(this.yLabels, function(label, index){
1810
+ // Don't draw a centre value
1811
+ if (index > 0){
1812
+ var yCenterOffset = index * (this.drawingArea/this.steps),
1813
+ yHeight = this.yCenter - yCenterOffset,
1814
+ pointPosition;
1815
+
1816
+ // Draw circular lines around the scale
1817
+ if (this.lineWidth > 0){
1818
+ ctx.strokeStyle = this.lineColor;
1819
+ ctx.lineWidth = this.lineWidth;
1820
+
1821
+ if(this.lineArc){
1822
+ ctx.beginPath();
1823
+ ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2);
1824
+ ctx.closePath();
1825
+ ctx.stroke();
1826
+ } else{
1827
+ ctx.beginPath();
1828
+ for (var i=0;i<this.valuesCount;i++)
1829
+ {
1830
+ pointPosition = this.getPointPosition(i, this.calculateCenterOffset(this.min + (index * this.stepValue)));
1831
+ if (i === 0){
1832
+ ctx.moveTo(pointPosition.x, pointPosition.y);
1833
+ } else {
1834
+ ctx.lineTo(pointPosition.x, pointPosition.y);
1835
+ }
1836
+ }
1837
+ ctx.closePath();
1838
+ ctx.stroke();
1839
+ }
1840
+ }
1841
+ if(this.showLabels){
1842
+ ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily);
1843
+ if (this.showLabelBackdrop){
1844
+ var labelWidth = ctx.measureText(label).width;
1845
+ ctx.fillStyle = this.backdropColor;
1846
+ ctx.fillRect(
1847
+ this.xCenter - labelWidth/2 - this.backdropPaddingX,
1848
+ yHeight - this.fontSize/2 - this.backdropPaddingY,
1849
+ labelWidth + this.backdropPaddingX*2,
1850
+ this.fontSize + this.backdropPaddingY*2
1851
+ );
1852
+ }
1853
+ ctx.textAlign = 'center';
1854
+ ctx.textBaseline = "middle";
1855
+ ctx.fillStyle = this.fontColor;
1856
+ ctx.fillText(label, this.xCenter, yHeight);
1857
+ }
1858
+ }
1859
+ }, this);
1860
+
1861
+ if (!this.lineArc){
1862
+ ctx.lineWidth = this.angleLineWidth;
1863
+ ctx.strokeStyle = this.angleLineColor;
1864
+ for (var i = this.valuesCount - 1; i >= 0; i--) {
1865
+ if (this.angleLineWidth > 0){
1866
+ var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max));
1867
+ ctx.beginPath();
1868
+ ctx.moveTo(this.xCenter, this.yCenter);
1869
+ ctx.lineTo(outerPosition.x, outerPosition.y);
1870
+ ctx.stroke();
1871
+ ctx.closePath();
1872
+ }
1873
+ // Extra 3px out for some label spacing
1874
+ var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5);
1875
+ ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily);
1876
+ ctx.fillStyle = this.pointLabelFontColor;
1877
+
1878
+ var labelsCount = this.labels.length,
1879
+ halfLabelsCount = this.labels.length/2,
1880
+ quarterLabelsCount = halfLabelsCount/2,
1881
+ upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount),
1882
+ exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount);
1883
+ if (i === 0){
1884
+ ctx.textAlign = 'center';
1885
+ } else if(i === halfLabelsCount){
1886
+ ctx.textAlign = 'center';
1887
+ } else if (i < halfLabelsCount){
1888
+ ctx.textAlign = 'left';
1889
+ } else {
1890
+ ctx.textAlign = 'right';
1891
+ }
1892
+
1893
+ // Set the correct text baseline based on outer positioning
1894
+ if (exactQuarter){
1895
+ ctx.textBaseline = 'middle';
1896
+ } else if (upperHalf){
1897
+ ctx.textBaseline = 'bottom';
1898
+ } else {
1899
+ ctx.textBaseline = 'top';
1900
+ }
1901
+
1902
+ ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y);
1903
+ }
1904
+ }
1905
+ }
1906
+ }
1907
+ });
1908
+
1909
+ // Attach global event to resize each chart instance when the browser resizes
1910
+ helpers.addEvent(window, "resize", (function(){
1911
+ // Basic debounce of resize function so it doesn't hurt performance when resizing browser.
1912
+ var timeout;
1913
+ return function(){
1914
+ clearTimeout(timeout);
1915
+ timeout = setTimeout(function(){
1916
+ each(Chart.instances,function(instance){
1917
+ // If the responsive flag is set in the chart instance config
1918
+ // Cascade the resize event down to the chart.
1919
+ if (instance.options.responsive){
1920
+ instance.resize(instance.render, true);
1921
+ }
1922
+ });
1923
+ }, 50);
1924
+ };
1925
+ })());
1926
+
1927
+
1928
+ if (amd) {
1929
+ define(function(){
1930
+ return Chart;
1931
+ });
1932
+ } else if (typeof module === 'object' && module.exports) {
1933
+ module.exports = Chart;
1934
+ }
1935
+
1936
+ root.Chart = Chart;
1937
+
1938
+ Chart.noConflict = function(){
1939
+ root.Chart = previous;
1940
+ return Chart;
1941
+ };
1942
+
1943
+ }).call(this);
1944
+
1945
+ (function(){
1946
+ "use strict";
1947
+
1948
+ var root = this,
1949
+ Chart = root.Chart,
1950
+ helpers = Chart.helpers;
1951
+
1952
+
1953
+ var defaultConfig = {
1954
+ //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
1955
+ scaleBeginAtZero : true,
1956
+
1957
+ //Boolean - Whether grid lines are shown across the chart
1958
+ scaleShowGridLines : true,
1959
+
1960
+ //String - Colour of the grid lines
1961
+ scaleGridLineColor : "rgba(0,0,0,.05)",
1962
+
1963
+ //Number - Width of the grid lines
1964
+ scaleGridLineWidth : 1,
1965
+
1966
+ //Boolean - If there is a stroke on each bar
1967
+ barShowStroke : true,
1968
+
1969
+ //Number - Pixel width of the bar stroke
1970
+ barStrokeWidth : 2,
1971
+
1972
+ //Number - Spacing between each of the X value sets
1973
+ barValueSpacing : 5,
1974
+
1975
+ //Number - Spacing between data sets within X values
1976
+ barDatasetSpacing : 1,
1977
+
1978
+ //String - A legend template
1979
+ legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].fillColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>"
1980
+
1981
+ };
1982
+
1983
+
1984
+ Chart.Type.extend({
1985
+ name: "Bar",
1986
+ defaults : defaultConfig,
1987
+ initialize: function(data){
1988
+
1989
+ //Expose options as a scope variable here so we can access it in the ScaleClass
1990
+ var options = this.options;
1991
+
1992
+ this.ScaleClass = Chart.Scale.extend({
1993
+ offsetGridLines : true,
1994
+ calculateBarX : function(datasetCount, datasetIndex, barIndex){
1995
+ //Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar
1996
+ var xWidth = this.calculateBaseWidth(),
1997
+ xAbsolute = this.calculateX(barIndex) - (xWidth/2),
1998
+ barWidth = this.calculateBarWidth(datasetCount);
1999
+
2000
+ return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2;
2001
+ },
2002
+ calculateBaseWidth : function(){
2003
+ return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing);
2004
+ },
2005
+ calculateBarWidth : function(datasetCount){
2006
+ //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset
2007
+ var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing);
2008
+
2009
+ return (baseWidth / datasetCount);
2010
+ }
2011
+ });
2012
+
2013
+ this.datasets = [];
2014
+
2015
+ //Set up tooltip events on the chart
2016
+ if (this.options.showTooltips){
2017
+ helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
2018
+ var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : [];
2019
+
2020
+ this.eachBars(function(bar){
2021
+ bar.restore(['fillColor', 'strokeColor']);
2022
+ });
2023
+ helpers.each(activeBars, function(activeBar){
2024
+ activeBar.fillColor = activeBar.highlightFill;
2025
+ activeBar.strokeColor = activeBar.highlightStroke;
2026
+ });
2027
+ this.showTooltip(activeBars);
2028
+ });
2029
+ }
2030
+
2031
+ //Declare the extension of the default point, to cater for the options passed in to the constructor
2032
+ this.BarClass = Chart.Rectangle.extend({
2033
+ strokeWidth : this.options.barStrokeWidth,
2034
+ showStroke : this.options.barShowStroke,
2035
+ ctx : this.chart.ctx
2036
+ });
2037
+
2038
+ //Iterate through each of the datasets, and build this into a property of the chart
2039
+ helpers.each(data.datasets,function(dataset,datasetIndex){
2040
+
2041
+ var datasetObject = {
2042
+ label : dataset.label || null,
2043
+ fillColor : dataset.fillColor,
2044
+ strokeColor : dataset.strokeColor,
2045
+ bars : []
2046
+ };
2047
+
2048
+ this.datasets.push(datasetObject);
2049
+
2050
+ helpers.each(dataset.data,function(dataPoint,index){
2051
+ //Add a new point for each piece of data, passing any required data to draw.
2052
+ datasetObject.bars.push(new this.BarClass({
2053
+ value : dataPoint,
2054
+ label : data.labels[index],
2055
+ datasetLabel: dataset.label,
2056
+ strokeColor : dataset.strokeColor,
2057
+ fillColor : dataset.fillColor,
2058
+ highlightFill : dataset.highlightFill || dataset.fillColor,
2059
+ highlightStroke : dataset.highlightStroke || dataset.strokeColor
2060
+ }));
2061
+ },this);
2062
+
2063
+ },this);
2064
+
2065
+ this.buildScale(data.labels);
2066
+
2067
+ this.BarClass.prototype.base = this.scale.endPoint;
2068
+
2069
+ this.eachBars(function(bar, index, datasetIndex){
2070
+ helpers.extend(bar, {
2071
+ width : this.scale.calculateBarWidth(this.datasets.length),
2072
+ x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index),
2073
+ y: this.scale.endPoint
2074
+ });
2075
+ bar.save();
2076
+ }, this);
2077
+
2078
+ this.render();
2079
+ },
2080
+ update : function(){
2081
+ this.scale.update();
2082
+ // Reset any highlight colours before updating.
2083
+ helpers.each(this.activeElements, function(activeElement){
2084
+ activeElement.restore(['fillColor', 'strokeColor']);
2085
+ });
2086
+
2087
+ this.eachBars(function(bar){
2088
+ bar.save();
2089
+ });
2090
+ this.render();
2091
+ },
2092
+ eachBars : function(callback){
2093
+ helpers.each(this.datasets,function(dataset, datasetIndex){
2094
+ helpers.each(dataset.bars, callback, this, datasetIndex);
2095
+ },this);
2096
+ },
2097
+ getBarsAtEvent : function(e){
2098
+ var barsArray = [],
2099
+ eventPosition = helpers.getRelativePosition(e),
2100
+ datasetIterator = function(dataset){
2101
+ barsArray.push(dataset.bars[barIndex]);
2102
+ },
2103
+ barIndex;
2104
+
2105
+ for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) {
2106
+ for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) {
2107
+ if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){
2108
+ helpers.each(this.datasets, datasetIterator);
2109
+ return barsArray;
2110
+ }
2111
+ }
2112
+ }
2113
+
2114
+ return barsArray;
2115
+ },
2116
+ buildScale : function(labels){
2117
+ var self = this;
2118
+
2119
+ var dataTotal = function(){
2120
+ var values = [];
2121
+ self.eachBars(function(bar){
2122
+ values.push(bar.value);
2123
+ });
2124
+ return values;
2125
+ };
2126
+
2127
+ var scaleOptions = {
2128
+ templateString : this.options.scaleLabel,
2129
+ height : this.chart.height,
2130
+ width : this.chart.width,
2131
+ ctx : this.chart.ctx,
2132
+ textColor : this.options.scaleFontColor,
2133
+ fontSize : this.options.scaleFontSize,
2134
+ fontStyle : this.options.scaleFontStyle,
2135
+ fontFamily : this.options.scaleFontFamily,
2136
+ valuesCount : labels.length,
2137
+ beginAtZero : this.options.scaleBeginAtZero,
2138
+ integersOnly : this.options.scaleIntegersOnly,
2139
+ calculateYRange: function(currentHeight){
2140
+ var updatedRanges = helpers.calculateScaleRange(
2141
+ dataTotal(),
2142
+ currentHeight,
2143
+ this.fontSize,
2144
+ this.beginAtZero,
2145
+ this.integersOnly
2146
+ );
2147
+ helpers.extend(this, updatedRanges);
2148
+ },
2149
+ xLabels : labels,
2150
+ font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily),
2151
+ lineWidth : this.options.scaleLineWidth,
2152
+ lineColor : this.options.scaleLineColor,
2153
+ gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0,
2154
+ gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)",
2155
+ padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0,
2156
+ showLabels : this.options.scaleShowLabels,
2157
+ display : this.options.showScale
2158
+ };
2159
+
2160
+ if (this.options.scaleOverride){
2161
+ helpers.extend(scaleOptions, {
2162
+ calculateYRange: helpers.noop,
2163
+ steps: this.options.scaleSteps,
2164
+ stepValue: this.options.scaleStepWidth,
2165
+ min: this.options.scaleStartValue,
2166
+ max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
2167
+ });
2168
+ }
2169
+
2170
+ this.scale = new this.ScaleClass(scaleOptions);
2171
+ },
2172
+ addData : function(valuesArray,label){
2173
+ //Map the values array for each of the datasets
2174
+ helpers.each(valuesArray,function(value,datasetIndex){
2175
+ //Add a new point for each piece of data, passing any required data to draw.
2176
+ this.datasets[datasetIndex].bars.push(new this.BarClass({
2177
+ value : value,
2178
+ label : label,
2179
+ x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1),
2180
+ y: this.scale.endPoint,
2181
+ width : this.scale.calculateBarWidth(this.datasets.length),
2182
+ base : this.scale.endPoint,
2183
+ strokeColor : this.datasets[datasetIndex].strokeColor,
2184
+ fillColor : this.datasets[datasetIndex].fillColor
2185
+ }));
2186
+ },this);
2187
+
2188
+ this.scale.addXLabel(label);
2189
+ //Then re-render the chart.
2190
+ this.update();
2191
+ },
2192
+ removeData : function(){
2193
+ this.scale.removeXLabel();
2194
+ //Then re-render the chart.
2195
+ helpers.each(this.datasets,function(dataset){
2196
+ dataset.bars.shift();
2197
+ },this);
2198
+ this.update();
2199
+ },
2200
+ reflow : function(){
2201
+ helpers.extend(this.BarClass.prototype,{
2202
+ y: this.scale.endPoint,
2203
+ base : this.scale.endPoint
2204
+ });
2205
+ var newScaleProps = helpers.extend({
2206
+ height : this.chart.height,
2207
+ width : this.chart.width
2208
+ });
2209
+ this.scale.update(newScaleProps);
2210
+ },
2211
+ draw : function(ease){
2212
+ var easingDecimal = ease || 1;
2213
+ this.clear();
2214
+
2215
+ var ctx = this.chart.ctx;
2216
+
2217
+ this.scale.draw(easingDecimal);
2218
+
2219
+ //Draw all the bars for each dataset
2220
+ helpers.each(this.datasets,function(dataset,datasetIndex){
2221
+ helpers.each(dataset.bars,function(bar,index){
2222
+ if (bar.hasValue()){
2223
+ bar.base = this.scale.endPoint;
2224
+ //Transition then draw
2225
+ bar.transition({
2226
+ x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index),
2227
+ y : this.scale.calculateY(bar.value),
2228
+ width : this.scale.calculateBarWidth(this.datasets.length)
2229
+ }, easingDecimal).draw();
2230
+ }
2231
+ },this);
2232
+
2233
+ },this);
2234
+ }
2235
+ });
2236
+
2237
+
2238
+ }).call(this);
2239
+ (function(){
2240
+ "use strict";
2241
+
2242
+ var root = this,
2243
+ Chart = root.Chart,
2244
+ //Cache a local reference to Chart.helpers
2245
+ helpers = Chart.helpers;
2246
+
2247
+ var defaultConfig = {
2248
+ //Boolean - Whether we should show a stroke on each segment
2249
+ segmentShowStroke : true,
2250
+
2251
+ //String - The colour of each segment stroke
2252
+ segmentStrokeColor : "#fff",
2253
+
2254
+ //Number - The width of each segment stroke
2255
+ segmentStrokeWidth : 2,
2256
+
2257
+ //The percentage of the chart that we cut out of the middle.
2258
+ percentageInnerCutout : 50,
2259
+
2260
+ //Number - Amount of animation steps
2261
+ animationSteps : 100,
2262
+
2263
+ //String - Animation easing effect
2264
+ animationEasing : "easeOutBounce",
2265
+
2266
+ //Boolean - Whether we animate the rotation of the Doughnut
2267
+ animateRotate : true,
2268
+
2269
+ //Boolean - Whether we animate scaling the Doughnut from the centre
2270
+ animateScale : false,
2271
+
2272
+ //String - A legend template
2273
+ legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>"
2274
+
2275
+ };
2276
+
2277
+
2278
+ Chart.Type.extend({
2279
+ //Passing in a name registers this chart in the Chart namespace
2280
+ name: "Doughnut",
2281
+ //Providing a defaults will also register the deafults in the chart namespace
2282
+ defaults : defaultConfig,
2283
+ //Initialize is fired when the chart is initialized - Data is passed in as a parameter
2284
+ //Config is automatically merged by the core of Chart.js, and is available at this.options
2285
+ initialize: function(data){
2286
+
2287
+ //Declare segments as a static property to prevent inheriting across the Chart type prototype
2288
+ this.segments = [];
2289
+ this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2;
2290
+
2291
+ this.SegmentArc = Chart.Arc.extend({
2292
+ ctx : this.chart.ctx,
2293
+ x : this.chart.width/2,
2294
+ y : this.chart.height/2
2295
+ });
2296
+
2297
+ //Set up tooltip events on the chart
2298
+ if (this.options.showTooltips){
2299
+ helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
2300
+ var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : [];
2301
+
2302
+ helpers.each(this.segments,function(segment){
2303
+ segment.restore(["fillColor"]);
2304
+ });
2305
+ helpers.each(activeSegments,function(activeSegment){
2306
+ activeSegment.fillColor = activeSegment.highlightColor;
2307
+ });
2308
+ this.showTooltip(activeSegments);
2309
+ });
2310
+ }
2311
+ this.calculateTotal(data);
2312
+
2313
+ helpers.each(data,function(datapoint, index){
2314
+ this.addData(datapoint, index, true);
2315
+ },this);
2316
+
2317
+ this.render();
2318
+ },
2319
+ getSegmentsAtEvent : function(e){
2320
+ var segmentsArray = [];
2321
+
2322
+ var location = helpers.getRelativePosition(e);
2323
+
2324
+ helpers.each(this.segments,function(segment){
2325
+ if (segment.inRange(location.x,location.y)) segmentsArray.push(segment);
2326
+ },this);
2327
+ return segmentsArray;
2328
+ },
2329
+ addData : function(segment, atIndex, silent){
2330
+ var index = atIndex || this.segments.length;
2331
+ this.segments.splice(index, 0, new this.SegmentArc({
2332
+ value : segment.value,
2333
+ outerRadius : (this.options.animateScale) ? 0 : this.outerRadius,
2334
+ innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout,
2335
+ fillColor : segment.color,
2336
+ highlightColor : segment.highlight || segment.color,
2337
+ showStroke : this.options.segmentShowStroke,
2338
+ strokeWidth : this.options.segmentStrokeWidth,
2339
+ strokeColor : this.options.segmentStrokeColor,
2340
+ startAngle : Math.PI * 1.5,
2341
+ circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value),
2342
+ label : segment.label
2343
+ }));
2344
+ if (!silent){
2345
+ this.reflow();
2346
+ this.update();
2347
+ }
2348
+ },
2349
+ calculateCircumference : function(value){
2350
+ return (Math.PI*2)*(value / this.total);
2351
+ },
2352
+ calculateTotal : function(data){
2353
+ this.total = 0;
2354
+ helpers.each(data,function(segment){
2355
+ this.total += segment.value;
2356
+ },this);
2357
+ },
2358
+ update : function(){
2359
+ this.calculateTotal(this.segments);
2360
+
2361
+ // Reset any highlight colours before updating.
2362
+ helpers.each(this.activeElements, function(activeElement){
2363
+ activeElement.restore(['fillColor']);
2364
+ });
2365
+
2366
+ helpers.each(this.segments,function(segment){
2367
+ segment.save();
2368
+ });
2369
+ this.render();
2370
+ },
2371
+
2372
+ removeData: function(atIndex){
2373
+ var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1;
2374
+ this.segments.splice(indexToDelete, 1);
2375
+ this.reflow();
2376
+ this.update();
2377
+ },
2378
+
2379
+ reflow : function(){
2380
+ helpers.extend(this.SegmentArc.prototype,{
2381
+ x : this.chart.width/2,
2382
+ y : this.chart.height/2
2383
+ });
2384
+ this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2;
2385
+ helpers.each(this.segments, function(segment){
2386
+ segment.update({
2387
+ outerRadius : this.outerRadius,
2388
+ innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout
2389
+ });
2390
+ }, this);
2391
+ },
2392
+ draw : function(easeDecimal){
2393
+ var animDecimal = (easeDecimal) ? easeDecimal : 1;
2394
+ this.clear();
2395
+ helpers.each(this.segments,function(segment,index){
2396
+ segment.transition({
2397
+ circumference : this.calculateCircumference(segment.value),
2398
+ outerRadius : this.outerRadius,
2399
+ innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout
2400
+ },animDecimal);
2401
+
2402
+ segment.endAngle = segment.startAngle + segment.circumference;
2403
+
2404
+ segment.draw();
2405
+ if (index === 0){
2406
+ segment.startAngle = Math.PI * 1.5;
2407
+ }
2408
+ //Check to see if it's the last segment, if not get the next and update the start angle
2409
+ if (index < this.segments.length-1){
2410
+ this.segments[index+1].startAngle = segment.endAngle;
2411
+ }
2412
+ },this);
2413
+
2414
+ }
2415
+ });
2416
+
2417
+ Chart.types.Doughnut.extend({
2418
+ name : "Pie",
2419
+ defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0})
2420
+ });
2421
+
2422
+ }).call(this);
2423
+ (function(){
2424
+ "use strict";
2425
+
2426
+ var root = this,
2427
+ Chart = root.Chart,
2428
+ helpers = Chart.helpers;
2429
+
2430
+ var defaultConfig = {
2431
+
2432
+ ///Boolean - Whether grid lines are shown across the chart
2433
+ scaleShowGridLines : true,
2434
+
2435
+ //String - Colour of the grid lines
2436
+ scaleGridLineColor : "rgba(0,0,0,.05)",
2437
+
2438
+ //Number - Width of the grid lines
2439
+ scaleGridLineWidth : 1,
2440
+
2441
+ //Boolean - Whether the line is curved between points
2442
+ bezierCurve : true,
2443
+
2444
+ //Number - Tension of the bezier curve between points
2445
+ bezierCurveTension : 0.4,
2446
+
2447
+ //Boolean - Whether to show a dot for each point
2448
+ pointDot : true,
2449
+
2450
+ //Number - Radius of each point dot in pixels
2451
+ pointDotRadius : 4,
2452
+
2453
+ //Number - Pixel width of point dot stroke
2454
+ pointDotStrokeWidth : 1,
2455
+
2456
+ //Number - amount extra to add to the radius to cater for hit detection outside the drawn point
2457
+ pointHitDetectionRadius : 20,
2458
+
2459
+ //Boolean - Whether to show a stroke for datasets
2460
+ datasetStroke : true,
2461
+
2462
+ //Number - Pixel width of dataset stroke
2463
+ datasetStrokeWidth : 2,
2464
+
2465
+ //Boolean - Whether to fill the dataset with a colour
2466
+ datasetFill : true,
2467
+
2468
+ //String - A legend template
2469
+ legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>"
2470
+
2471
+ };
2472
+
2473
+
2474
+ Chart.Type.extend({
2475
+ name: "Line",
2476
+ defaults : defaultConfig,
2477
+ initialize: function(data){
2478
+ //Declare the extension of the default point, to cater for the options passed in to the constructor
2479
+ this.PointClass = Chart.Point.extend({
2480
+ strokeWidth : this.options.pointDotStrokeWidth,
2481
+ radius : this.options.pointDotRadius,
2482
+ display: this.options.pointDot,
2483
+ hitDetectionRadius : this.options.pointHitDetectionRadius,
2484
+ ctx : this.chart.ctx,
2485
+ inRange : function(mouseX){
2486
+ return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2));
2487
+ }
2488
+ });
2489
+
2490
+ this.datasets = [];
2491
+
2492
+ //Set up tooltip events on the chart
2493
+ if (this.options.showTooltips){
2494
+ helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
2495
+ var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : [];
2496
+ this.eachPoints(function(point){
2497
+ point.restore(['fillColor', 'strokeColor']);
2498
+ });
2499
+ helpers.each(activePoints, function(activePoint){
2500
+ activePoint.fillColor = activePoint.highlightFill;
2501
+ activePoint.strokeColor = activePoint.highlightStroke;
2502
+ });
2503
+ this.showTooltip(activePoints);
2504
+ });
2505
+ }
2506
+
2507
+ //Iterate through each of the datasets, and build this into a property of the chart
2508
+ helpers.each(data.datasets,function(dataset){
2509
+
2510
+ var datasetObject = {
2511
+ label : dataset.label || null,
2512
+ fillColor : dataset.fillColor,
2513
+ strokeColor : dataset.strokeColor,
2514
+ pointColor : dataset.pointColor,
2515
+ pointStrokeColor : dataset.pointStrokeColor,
2516
+ points : []
2517
+ };
2518
+
2519
+ this.datasets.push(datasetObject);
2520
+
2521
+
2522
+ helpers.each(dataset.data,function(dataPoint,index){
2523
+ //Add a new point for each piece of data, passing any required data to draw.
2524
+ datasetObject.points.push(new this.PointClass({
2525
+ value : dataPoint,
2526
+ label : data.labels[index],
2527
+ datasetLabel: dataset.label,
2528
+ strokeColor : dataset.pointStrokeColor,
2529
+ fillColor : dataset.pointColor,
2530
+ highlightFill : dataset.pointHighlightFill || dataset.pointColor,
2531
+ highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor
2532
+ }));
2533
+ },this);
2534
+
2535
+ this.buildScale(data.labels);
2536
+
2537
+
2538
+ this.eachPoints(function(point, index){
2539
+ helpers.extend(point, {
2540
+ x: this.scale.calculateX(index),
2541
+ y: this.scale.endPoint
2542
+ });
2543
+ point.save();
2544
+ }, this);
2545
+
2546
+ },this);
2547
+
2548
+
2549
+ this.render();
2550
+ },
2551
+ update : function(){
2552
+ this.scale.update();
2553
+ // Reset any highlight colours before updating.
2554
+ helpers.each(this.activeElements, function(activeElement){
2555
+ activeElement.restore(['fillColor', 'strokeColor']);
2556
+ });
2557
+ this.eachPoints(function(point){
2558
+ point.save();
2559
+ });
2560
+ this.render();
2561
+ },
2562
+ eachPoints : function(callback){
2563
+ helpers.each(this.datasets,function(dataset){
2564
+ helpers.each(dataset.points,callback,this);
2565
+ },this);
2566
+ },
2567
+ getPointsAtEvent : function(e){
2568
+ var pointsArray = [],
2569
+ eventPosition = helpers.getRelativePosition(e);
2570
+ helpers.each(this.datasets,function(dataset){
2571
+ helpers.each(dataset.points,function(point){
2572
+ if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point);
2573
+ });
2574
+ },this);
2575
+ return pointsArray;
2576
+ },
2577
+ buildScale : function(labels){
2578
+ var self = this;
2579
+
2580
+ var dataTotal = function(){
2581
+ var values = [];
2582
+ self.eachPoints(function(point){
2583
+ values.push(point.value);
2584
+ });
2585
+
2586
+ return values;
2587
+ };
2588
+
2589
+ var scaleOptions = {
2590
+ templateString : this.options.scaleLabel,
2591
+ height : this.chart.height,
2592
+ width : this.chart.width,
2593
+ ctx : this.chart.ctx,
2594
+ textColor : this.options.scaleFontColor,
2595
+ fontSize : this.options.scaleFontSize,
2596
+ fontStyle : this.options.scaleFontStyle,
2597
+ fontFamily : this.options.scaleFontFamily,
2598
+ valuesCount : labels.length,
2599
+ beginAtZero : this.options.scaleBeginAtZero,
2600
+ integersOnly : this.options.scaleIntegersOnly,
2601
+ calculateYRange : function(currentHeight){
2602
+ var updatedRanges = helpers.calculateScaleRange(
2603
+ dataTotal(),
2604
+ currentHeight,
2605
+ this.fontSize,
2606
+ this.beginAtZero,
2607
+ this.integersOnly
2608
+ );
2609
+ helpers.extend(this, updatedRanges);
2610
+ },
2611
+ xLabels : labels,
2612
+ font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily),
2613
+ lineWidth : this.options.scaleLineWidth,
2614
+ lineColor : this.options.scaleLineColor,
2615
+ gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0,
2616
+ gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)",
2617
+ padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth,
2618
+ showLabels : this.options.scaleShowLabels,
2619
+ display : this.options.showScale
2620
+ };
2621
+
2622
+ if (this.options.scaleOverride){
2623
+ helpers.extend(scaleOptions, {
2624
+ calculateYRange: helpers.noop,
2625
+ steps: this.options.scaleSteps,
2626
+ stepValue: this.options.scaleStepWidth,
2627
+ min: this.options.scaleStartValue,
2628
+ max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
2629
+ });
2630
+ }
2631
+
2632
+
2633
+ this.scale = new Chart.Scale(scaleOptions);
2634
+ },
2635
+ addData : function(valuesArray,label){
2636
+ //Map the values array for each of the datasets
2637
+
2638
+ helpers.each(valuesArray,function(value,datasetIndex){
2639
+ //Add a new point for each piece of data, passing any required data to draw.
2640
+ this.datasets[datasetIndex].points.push(new this.PointClass({
2641
+ value : value,
2642
+ label : label,
2643
+ x: this.scale.calculateX(this.scale.valuesCount+1),
2644
+ y: this.scale.endPoint,
2645
+ strokeColor : this.datasets[datasetIndex].pointStrokeColor,
2646
+ fillColor : this.datasets[datasetIndex].pointColor
2647
+ }));
2648
+ },this);
2649
+
2650
+ this.scale.addXLabel(label);
2651
+ //Then re-render the chart.
2652
+ this.update();
2653
+ },
2654
+ removeData : function(){
2655
+ this.scale.removeXLabel();
2656
+ //Then re-render the chart.
2657
+ helpers.each(this.datasets,function(dataset){
2658
+ dataset.points.shift();
2659
+ },this);
2660
+ this.update();
2661
+ },
2662
+ reflow : function(){
2663
+ var newScaleProps = helpers.extend({
2664
+ height : this.chart.height,
2665
+ width : this.chart.width
2666
+ });
2667
+ this.scale.update(newScaleProps);
2668
+ },
2669
+ draw : function(ease){
2670
+ var easingDecimal = ease || 1;
2671
+ this.clear();
2672
+
2673
+ var ctx = this.chart.ctx;
2674
+
2675
+ // Some helper methods for getting the next/prev points
2676
+ var hasValue = function(item){
2677
+ return item.value !== null;
2678
+ },
2679
+ nextPoint = function(point, collection, index){
2680
+ return helpers.findNextWhere(collection, hasValue, index) || point;
2681
+ },
2682
+ previousPoint = function(point, collection, index){
2683
+ return helpers.findPreviousWhere(collection, hasValue, index) || point;
2684
+ };
2685
+
2686
+ this.scale.draw(easingDecimal);
2687
+
2688
+
2689
+ helpers.each(this.datasets,function(dataset){
2690
+ var pointsWithValues = helpers.where(dataset.points, hasValue);
2691
+
2692
+ //Transition each point first so that the line and point drawing isn't out of sync
2693
+ //We can use this extra loop to calculate the control points of this dataset also in this loop
2694
+
2695
+ helpers.each(dataset.points, function(point, index){
2696
+ if (point.hasValue()){
2697
+ point.transition({
2698
+ y : this.scale.calculateY(point.value),
2699
+ x : this.scale.calculateX(index)
2700
+ }, easingDecimal);
2701
+ }
2702
+ },this);
2703
+
2704
+
2705
+ // Control points need to be calculated in a seperate loop, because we need to know the current x/y of the point
2706
+ // This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed
2707
+ if (this.options.bezierCurve){
2708
+ helpers.each(pointsWithValues, function(point, index){
2709
+ var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0;
2710
+ point.controlPoints = helpers.splineCurve(
2711
+ previousPoint(point, pointsWithValues, index),
2712
+ point,
2713
+ nextPoint(point, pointsWithValues, index),
2714
+ tension
2715
+ );
2716
+
2717
+ // Prevent the bezier going outside of the bounds of the graph
2718
+
2719
+ // Cap puter bezier handles to the upper/lower scale bounds
2720
+ if (point.controlPoints.outer.y > this.scale.endPoint){
2721
+ point.controlPoints.outer.y = this.scale.endPoint;
2722
+ }
2723
+ else if (point.controlPoints.outer.y < this.scale.startPoint){
2724
+ point.controlPoints.outer.y = this.scale.startPoint;
2725
+ }
2726
+
2727
+ // Cap inner bezier handles to the upper/lower scale bounds
2728
+ if (point.controlPoints.inner.y > this.scale.endPoint){
2729
+ point.controlPoints.inner.y = this.scale.endPoint;
2730
+ }
2731
+ else if (point.controlPoints.inner.y < this.scale.startPoint){
2732
+ point.controlPoints.inner.y = this.scale.startPoint;
2733
+ }
2734
+ },this);
2735
+ }
2736
+
2737
+
2738
+ //Draw the line between all the points
2739
+ ctx.lineWidth = this.options.datasetStrokeWidth;
2740
+ ctx.strokeStyle = dataset.strokeColor;
2741
+ ctx.beginPath();
2742
+
2743
+ helpers.each(pointsWithValues, function(point, index){
2744
+ if (index === 0){
2745
+ ctx.moveTo(point.x, point.y);
2746
+ }
2747
+ else{
2748
+ if(this.options.bezierCurve){
2749
+ var previous = previousPoint(point, pointsWithValues, index);
2750
+
2751
+ ctx.bezierCurveTo(
2752
+ previous.controlPoints.outer.x,
2753
+ previous.controlPoints.outer.y,
2754
+ point.controlPoints.inner.x,
2755
+ point.controlPoints.inner.y,
2756
+ point.x,
2757
+ point.y
2758
+ );
2759
+ }
2760
+ else{
2761
+ ctx.lineTo(point.x,point.y);
2762
+ }
2763
+ }
2764
+ }, this);
2765
+
2766
+ ctx.stroke();
2767
+
2768
+ if (this.options.datasetFill && pointsWithValues.length > 0){
2769
+ //Round off the line by going to the base of the chart, back to the start, then fill.
2770
+ ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint);
2771
+ ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint);
2772
+ ctx.fillStyle = dataset.fillColor;
2773
+ ctx.closePath();
2774
+ ctx.fill();
2775
+ }
2776
+
2777
+ //Now draw the points over the line
2778
+ //A little inefficient double looping, but better than the line
2779
+ //lagging behind the point positions
2780
+ helpers.each(pointsWithValues,function(point){
2781
+ point.draw();
2782
+ });
2783
+ },this);
2784
+ }
2785
+ });
2786
+
2787
+
2788
+ }).call(this);
2789
+ (function(){
2790
+ "use strict";
2791
+
2792
+ var root = this,
2793
+ Chart = root.Chart,
2794
+ //Cache a local reference to Chart.helpers
2795
+ helpers = Chart.helpers;
2796
+
2797
+ var defaultConfig = {
2798
+ //Boolean - Show a backdrop to the scale label
2799
+ scaleShowLabelBackdrop : true,
2800
+
2801
+ //String - The colour of the label backdrop
2802
+ scaleBackdropColor : "rgba(255,255,255,0.75)",
2803
+
2804
+ // Boolean - Whether the scale should begin at zero
2805
+ scaleBeginAtZero : true,
2806
+
2807
+ //Number - The backdrop padding above & below the label in pixels
2808
+ scaleBackdropPaddingY : 2,
2809
+
2810
+ //Number - The backdrop padding to the side of the label in pixels
2811
+ scaleBackdropPaddingX : 2,
2812
+
2813
+ //Boolean - Show line for each value in the scale
2814
+ scaleShowLine : true,
2815
+
2816
+ //Boolean - Stroke a line around each segment in the chart
2817
+ segmentShowStroke : true,
2818
+
2819
+ //String - The colour of the stroke on each segement.
2820
+ segmentStrokeColor : "#fff",
2821
+
2822
+ //Number - The width of the stroke value in pixels
2823
+ segmentStrokeWidth : 2,
2824
+
2825
+ //Number - Amount of animation steps
2826
+ animationSteps : 100,
2827
+
2828
+ //String - Animation easing effect.
2829
+ animationEasing : "easeOutBounce",
2830
+
2831
+ //Boolean - Whether to animate the rotation of the chart
2832
+ animateRotate : true,
2833
+
2834
+ //Boolean - Whether to animate scaling the chart from the centre
2835
+ animateScale : false,
2836
+
2837
+ //String - A legend template
2838
+ legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>"
2839
+ };
2840
+
2841
+
2842
+ Chart.Type.extend({
2843
+ //Passing in a name registers this chart in the Chart namespace
2844
+ name: "PolarArea",
2845
+ //Providing a defaults will also register the deafults in the chart namespace
2846
+ defaults : defaultConfig,
2847
+ //Initialize is fired when the chart is initialized - Data is passed in as a parameter
2848
+ //Config is automatically merged by the core of Chart.js, and is available at this.options
2849
+ initialize: function(data){
2850
+ this.segments = [];
2851
+ //Declare segment class as a chart instance specific class, so it can share props for this instance
2852
+ this.SegmentArc = Chart.Arc.extend({
2853
+ showStroke : this.options.segmentShowStroke,
2854
+ strokeWidth : this.options.segmentStrokeWidth,
2855
+ strokeColor : this.options.segmentStrokeColor,
2856
+ ctx : this.chart.ctx,
2857
+ innerRadius : 0,
2858
+ x : this.chart.width/2,
2859
+ y : this.chart.height/2
2860
+ });
2861
+ this.scale = new Chart.RadialScale({
2862
+ display: this.options.showScale,
2863
+ fontStyle: this.options.scaleFontStyle,
2864
+ fontSize: this.options.scaleFontSize,
2865
+ fontFamily: this.options.scaleFontFamily,
2866
+ fontColor: this.options.scaleFontColor,
2867
+ showLabels: this.options.scaleShowLabels,
2868
+ showLabelBackdrop: this.options.scaleShowLabelBackdrop,
2869
+ backdropColor: this.options.scaleBackdropColor,
2870
+ backdropPaddingY : this.options.scaleBackdropPaddingY,
2871
+ backdropPaddingX: this.options.scaleBackdropPaddingX,
2872
+ lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0,
2873
+ lineColor: this.options.scaleLineColor,
2874
+ lineArc: true,
2875
+ width: this.chart.width,
2876
+ height: this.chart.height,
2877
+ xCenter: this.chart.width/2,
2878
+ yCenter: this.chart.height/2,
2879
+ ctx : this.chart.ctx,
2880
+ templateString: this.options.scaleLabel,
2881
+ valuesCount: data.length
2882
+ });
2883
+
2884
+ this.updateScaleRange(data);
2885
+
2886
+ this.scale.update();
2887
+
2888
+ helpers.each(data,function(segment,index){
2889
+ this.addData(segment,index,true);
2890
+ },this);
2891
+
2892
+ //Set up tooltip events on the chart
2893
+ if (this.options.showTooltips){
2894
+ helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
2895
+ var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : [];
2896
+ helpers.each(this.segments,function(segment){
2897
+ segment.restore(["fillColor"]);
2898
+ });
2899
+ helpers.each(activeSegments,function(activeSegment){
2900
+ activeSegment.fillColor = activeSegment.highlightColor;
2901
+ });
2902
+ this.showTooltip(activeSegments);
2903
+ });
2904
+ }
2905
+
2906
+ this.render();
2907
+ },
2908
+ getSegmentsAtEvent : function(e){
2909
+ var segmentsArray = [];
2910
+
2911
+ var location = helpers.getRelativePosition(e);
2912
+
2913
+ helpers.each(this.segments,function(segment){
2914
+ if (segment.inRange(location.x,location.y)) segmentsArray.push(segment);
2915
+ },this);
2916
+ return segmentsArray;
2917
+ },
2918
+ addData : function(segment, atIndex, silent){
2919
+ var index = atIndex || this.segments.length;
2920
+
2921
+ this.segments.splice(index, 0, new this.SegmentArc({
2922
+ fillColor: segment.color,
2923
+ highlightColor: segment.highlight || segment.color,
2924
+ label: segment.label,
2925
+ value: segment.value,
2926
+ outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value),
2927
+ circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(),
2928
+ startAngle: Math.PI * 1.5
2929
+ }));
2930
+ if (!silent){
2931
+ this.reflow();
2932
+ this.update();
2933
+ }
2934
+ },
2935
+ removeData: function(atIndex){
2936
+ var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1;
2937
+ this.segments.splice(indexToDelete, 1);
2938
+ this.reflow();
2939
+ this.update();
2940
+ },
2941
+ calculateTotal: function(data){
2942
+ this.total = 0;
2943
+ helpers.each(data,function(segment){
2944
+ this.total += segment.value;
2945
+ },this);
2946
+ this.scale.valuesCount = this.segments.length;
2947
+ },
2948
+ updateScaleRange: function(datapoints){
2949
+ var valuesArray = [];
2950
+ helpers.each(datapoints,function(segment){
2951
+ valuesArray.push(segment.value);
2952
+ });
2953
+
2954
+ var scaleSizes = (this.options.scaleOverride) ?
2955
+ {
2956
+ steps: this.options.scaleSteps,
2957
+ stepValue: this.options.scaleStepWidth,
2958
+ min: this.options.scaleStartValue,
2959
+ max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
2960
+ } :
2961
+ helpers.calculateScaleRange(
2962
+ valuesArray,
2963
+ helpers.min([this.chart.width, this.chart.height])/2,
2964
+ this.options.scaleFontSize,
2965
+ this.options.scaleBeginAtZero,
2966
+ this.options.scaleIntegersOnly
2967
+ );
2968
+
2969
+ helpers.extend(
2970
+ this.scale,
2971
+ scaleSizes,
2972
+ {
2973
+ size: helpers.min([this.chart.width, this.chart.height]),
2974
+ xCenter: this.chart.width/2,
2975
+ yCenter: this.chart.height/2
2976
+ }
2977
+ );
2978
+
2979
+ },
2980
+ update : function(){
2981
+ this.calculateTotal(this.segments);
2982
+
2983
+ helpers.each(this.segments,function(segment){
2984
+ segment.save();
2985
+ });
2986
+ this.render();
2987
+ },
2988
+ reflow : function(){
2989
+ helpers.extend(this.SegmentArc.prototype,{
2990
+ x : this.chart.width/2,
2991
+ y : this.chart.height/2
2992
+ });
2993
+ this.updateScaleRange(this.segments);
2994
+ this.scale.update();
2995
+
2996
+ helpers.extend(this.scale,{
2997
+ xCenter: this.chart.width/2,
2998
+ yCenter: this.chart.height/2
2999
+ });
3000
+
3001
+ helpers.each(this.segments, function(segment){
3002
+ segment.update({
3003
+ outerRadius : this.scale.calculateCenterOffset(segment.value)
3004
+ });
3005
+ }, this);
3006
+
3007
+ },
3008
+ draw : function(ease){
3009
+ var easingDecimal = ease || 1;
3010
+ //Clear & draw the canvas
3011
+ this.clear();
3012
+ helpers.each(this.segments,function(segment, index){
3013
+ segment.transition({
3014
+ circumference : this.scale.getCircumference(),
3015
+ outerRadius : this.scale.calculateCenterOffset(segment.value)
3016
+ },easingDecimal);
3017
+
3018
+ segment.endAngle = segment.startAngle + segment.circumference;
3019
+
3020
+ // If we've removed the first segment we need to set the first one to
3021
+ // start at the top.
3022
+ if (index === 0){
3023
+ segment.startAngle = Math.PI * 1.5;
3024
+ }
3025
+
3026
+ //Check to see if it's the last segment, if not get the next and update the start angle
3027
+ if (index < this.segments.length - 1){
3028
+ this.segments[index+1].startAngle = segment.endAngle;
3029
+ }
3030
+ segment.draw();
3031
+ }, this);
3032
+ this.scale.draw();
3033
+ }
3034
+ });
3035
+
3036
+ }).call(this);
3037
+ (function(){
3038
+ "use strict";
3039
+
3040
+ var root = this,
3041
+ Chart = root.Chart,
3042
+ helpers = Chart.helpers;
3043
+
3044
+
3045
+
3046
+ Chart.Type.extend({
3047
+ name: "Radar",
3048
+ defaults:{
3049
+ //Boolean - Whether to show lines for each scale point
3050
+ scaleShowLine : true,
3051
+
3052
+ //Boolean - Whether we show the angle lines out of the radar
3053
+ angleShowLineOut : true,
3054
+
3055
+ //Boolean - Whether to show labels on the scale
3056
+ scaleShowLabels : false,
3057
+
3058
+ // Boolean - Whether the scale should begin at zero
3059
+ scaleBeginAtZero : true,
3060
+
3061
+ //String - Colour of the angle line
3062
+ angleLineColor : "rgba(0,0,0,.1)",
3063
+
3064
+ //Number - Pixel width of the angle line
3065
+ angleLineWidth : 1,
3066
+
3067
+ //String - Point label font declaration
3068
+ pointLabelFontFamily : "'Arial'",
3069
+
3070
+ //String - Point label font weight
3071
+ pointLabelFontStyle : "normal",
3072
+
3073
+ //Number - Point label font size in pixels
3074
+ pointLabelFontSize : 10,
3075
+
3076
+ //String - Point label font colour
3077
+ pointLabelFontColor : "#666",
3078
+
3079
+ //Boolean - Whether to show a dot for each point
3080
+ pointDot : true,
3081
+
3082
+ //Number - Radius of each point dot in pixels
3083
+ pointDotRadius : 3,
3084
+
3085
+ //Number - Pixel width of point dot stroke
3086
+ pointDotStrokeWidth : 1,
3087
+
3088
+ //Number - amount extra to add to the radius to cater for hit detection outside the drawn point
3089
+ pointHitDetectionRadius : 20,
3090
+
3091
+ //Boolean - Whether to show a stroke for datasets
3092
+ datasetStroke : true,
3093
+
3094
+ //Number - Pixel width of dataset stroke
3095
+ datasetStrokeWidth : 2,
3096
+
3097
+ //Boolean - Whether to fill the dataset with a colour
3098
+ datasetFill : true,
3099
+
3100
+ //String - A legend template
3101
+ legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>"
3102
+
3103
+ },
3104
+
3105
+ initialize: function(data){
3106
+ this.PointClass = Chart.Point.extend({
3107
+ strokeWidth : this.options.pointDotStrokeWidth,
3108
+ radius : this.options.pointDotRadius,
3109
+ display: this.options.pointDot,
3110
+ hitDetectionRadius : this.options.pointHitDetectionRadius,
3111
+ ctx : this.chart.ctx
3112
+ });
3113
+
3114
+ this.datasets = [];
3115
+
3116
+ this.buildScale(data);
3117
+
3118
+ //Set up tooltip events on the chart
3119
+ if (this.options.showTooltips){
3120
+ helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
3121
+ var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : [];
3122
+
3123
+ this.eachPoints(function(point){
3124
+ point.restore(['fillColor', 'strokeColor']);
3125
+ });
3126
+ helpers.each(activePointsCollection, function(activePoint){
3127
+ activePoint.fillColor = activePoint.highlightFill;
3128
+ activePoint.strokeColor = activePoint.highlightStroke;
3129
+ });
3130
+
3131
+ this.showTooltip(activePointsCollection);
3132
+ });
3133
+ }
3134
+
3135
+ //Iterate through each of the datasets, and build this into a property of the chart
3136
+ helpers.each(data.datasets,function(dataset){
3137
+
3138
+ var datasetObject = {
3139
+ label: dataset.label || null,
3140
+ fillColor : dataset.fillColor,
3141
+ strokeColor : dataset.strokeColor,
3142
+ pointColor : dataset.pointColor,
3143
+ pointStrokeColor : dataset.pointStrokeColor,
3144
+ points : []
3145
+ };
3146
+
3147
+ this.datasets.push(datasetObject);
3148
+
3149
+ helpers.each(dataset.data,function(dataPoint,index){
3150
+ //Add a new point for each piece of data, passing any required data to draw.
3151
+ var pointPosition;
3152
+ if (!this.scale.animation){
3153
+ pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint));
3154
+ }
3155
+ datasetObject.points.push(new this.PointClass({
3156
+ value : dataPoint,
3157
+ label : data.labels[index],
3158
+ datasetLabel: dataset.label,
3159
+ x: (this.options.animation) ? this.scale.xCenter : pointPosition.x,
3160
+ y: (this.options.animation) ? this.scale.yCenter : pointPosition.y,
3161
+ strokeColor : dataset.pointStrokeColor,
3162
+ fillColor : dataset.pointColor,
3163
+ highlightFill : dataset.pointHighlightFill || dataset.pointColor,
3164
+ highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor
3165
+ }));
3166
+ },this);
3167
+
3168
+ },this);
3169
+
3170
+ this.render();
3171
+ },
3172
+ eachPoints : function(callback){
3173
+ helpers.each(this.datasets,function(dataset){
3174
+ helpers.each(dataset.points,callback,this);
3175
+ },this);
3176
+ },
3177
+
3178
+ getPointsAtEvent : function(evt){
3179
+ var mousePosition = helpers.getRelativePosition(evt),
3180
+ fromCenter = helpers.getAngleFromPoint({
3181
+ x: this.scale.xCenter,
3182
+ y: this.scale.yCenter
3183
+ }, mousePosition);
3184
+
3185
+ var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount,
3186
+ pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex),
3187
+ activePointsCollection = [];
3188
+
3189
+ // If we're at the top, make the pointIndex 0 to get the first of the array.
3190
+ if (pointIndex >= this.scale.valuesCount || pointIndex < 0){
3191
+ pointIndex = 0;
3192
+ }
3193
+
3194
+ if (fromCenter.distance <= this.scale.drawingArea){
3195
+ helpers.each(this.datasets, function(dataset){
3196
+ activePointsCollection.push(dataset.points[pointIndex]);
3197
+ });
3198
+ }
3199
+
3200
+ return activePointsCollection;
3201
+ },
3202
+
3203
+ buildScale : function(data){
3204
+ this.scale = new Chart.RadialScale({
3205
+ display: this.options.showScale,
3206
+ fontStyle: this.options.scaleFontStyle,
3207
+ fontSize: this.options.scaleFontSize,
3208
+ fontFamily: this.options.scaleFontFamily,
3209
+ fontColor: this.options.scaleFontColor,
3210
+ showLabels: this.options.scaleShowLabels,
3211
+ showLabelBackdrop: this.options.scaleShowLabelBackdrop,
3212
+ backdropColor: this.options.scaleBackdropColor,
3213
+ backdropPaddingY : this.options.scaleBackdropPaddingY,
3214
+ backdropPaddingX: this.options.scaleBackdropPaddingX,
3215
+ lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0,
3216
+ lineColor: this.options.scaleLineColor,
3217
+ angleLineColor : this.options.angleLineColor,
3218
+ angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0,
3219
+ // Point labels at the edge of each line
3220
+ pointLabelFontColor : this.options.pointLabelFontColor,
3221
+ pointLabelFontSize : this.options.pointLabelFontSize,
3222
+ pointLabelFontFamily : this.options.pointLabelFontFamily,
3223
+ pointLabelFontStyle : this.options.pointLabelFontStyle,
3224
+ height : this.chart.height,
3225
+ width: this.chart.width,
3226
+ xCenter: this.chart.width/2,
3227
+ yCenter: this.chart.height/2,
3228
+ ctx : this.chart.ctx,
3229
+ templateString: this.options.scaleLabel,
3230
+ labels: data.labels,
3231
+ valuesCount: data.datasets[0].data.length
3232
+ });
3233
+
3234
+ this.scale.setScaleSize();
3235
+ this.updateScaleRange(data.datasets);
3236
+ this.scale.buildYLabels();
3237
+ },
3238
+ updateScaleRange: function(datasets){
3239
+ var valuesArray = (function(){
3240
+ var totalDataArray = [];
3241
+ helpers.each(datasets,function(dataset){
3242
+ if (dataset.data){
3243
+ totalDataArray = totalDataArray.concat(dataset.data);
3244
+ }
3245
+ else {
3246
+ helpers.each(dataset.points, function(point){
3247
+ totalDataArray.push(point.value);
3248
+ });
3249
+ }
3250
+ });
3251
+ return totalDataArray;
3252
+ })();
3253
+
3254
+
3255
+ var scaleSizes = (this.options.scaleOverride) ?
3256
+ {
3257
+ steps: this.options.scaleSteps,
3258
+ stepValue: this.options.scaleStepWidth,
3259
+ min: this.options.scaleStartValue,
3260
+ max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
3261
+ } :
3262
+ helpers.calculateScaleRange(
3263
+ valuesArray,
3264
+ helpers.min([this.chart.width, this.chart.height])/2,
3265
+ this.options.scaleFontSize,
3266
+ this.options.scaleBeginAtZero,
3267
+ this.options.scaleIntegersOnly
3268
+ );
3269
+
3270
+ helpers.extend(
3271
+ this.scale,
3272
+ scaleSizes
3273
+ );
3274
+
3275
+ },
3276
+ addData : function(valuesArray,label){
3277
+ //Map the values array for each of the datasets
3278
+ this.scale.valuesCount++;
3279
+ helpers.each(valuesArray,function(value,datasetIndex){
3280
+ var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value));
3281
+ this.datasets[datasetIndex].points.push(new this.PointClass({
3282
+ value : value,
3283
+ label : label,
3284
+ x: pointPosition.x,
3285
+ y: pointPosition.y,
3286
+ strokeColor : this.datasets[datasetIndex].pointStrokeColor,
3287
+ fillColor : this.datasets[datasetIndex].pointColor
3288
+ }));
3289
+ },this);
3290
+
3291
+ this.scale.labels.push(label);
3292
+
3293
+ this.reflow();
3294
+
3295
+ this.update();
3296
+ },
3297
+ removeData : function(){
3298
+ this.scale.valuesCount--;
3299
+ this.scale.labels.shift();
3300
+ helpers.each(this.datasets,function(dataset){
3301
+ dataset.points.shift();
3302
+ },this);
3303
+ this.reflow();
3304
+ this.update();
3305
+ },
3306
+ update : function(){
3307
+ this.eachPoints(function(point){
3308
+ point.save();
3309
+ });
3310
+ this.reflow();
3311
+ this.render();
3312
+ },
3313
+ reflow: function(){
3314
+ helpers.extend(this.scale, {
3315
+ width : this.chart.width,
3316
+ height: this.chart.height,
3317
+ size : helpers.min([this.chart.width, this.chart.height]),
3318
+ xCenter: this.chart.width/2,
3319
+ yCenter: this.chart.height/2
3320
+ });
3321
+ this.updateScaleRange(this.datasets);
3322
+ this.scale.setScaleSize();
3323
+ this.scale.buildYLabels();
3324
+ },
3325
+ draw : function(ease){
3326
+ var easeDecimal = ease || 1,
3327
+ ctx = this.chart.ctx;
3328
+ this.clear();
3329
+ this.scale.draw();
3330
+
3331
+ helpers.each(this.datasets,function(dataset){
3332
+
3333
+ //Transition each point first so that the line and point drawing isn't out of sync
3334
+ helpers.each(dataset.points,function(point,index){
3335
+ if (point.hasValue()){
3336
+ point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal);
3337
+ }
3338
+ },this);
3339
+
3340
+
3341
+
3342
+ //Draw the line between all the points
3343
+ ctx.lineWidth = this.options.datasetStrokeWidth;
3344
+ ctx.strokeStyle = dataset.strokeColor;
3345
+ ctx.beginPath();
3346
+ helpers.each(dataset.points,function(point,index){
3347
+ if (index === 0){
3348
+ ctx.moveTo(point.x,point.y);
3349
+ }
3350
+ else{
3351
+ ctx.lineTo(point.x,point.y);
3352
+ }
3353
+ },this);
3354
+ ctx.closePath();
3355
+ ctx.stroke();
3356
+
3357
+ ctx.fillStyle = dataset.fillColor;
3358
+ ctx.fill();
3359
+
3360
+ //Now draw the points over the line
3361
+ //A little inefficient double looping, but better than the line
3362
+ //lagging behind the point positions
3363
+ helpers.each(dataset.points,function(point){
3364
+ if (point.hasValue()){
3365
+ point.draw();
3366
+ }
3367
+ });
3368
+
3369
+ },this);
3370
+
3371
+ }
3372
+
3373
+ });
3374
+
3375
+
3376
+
3377
+
3378
+
3379
+ }).call(this);
js/dashboard-chart.js ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function optimizationChecking(call_back) {
2
+ var ret;
3
+ jQuery.ajax({
4
+ url: myAjax.ajax_url,
5
+ async: false,
6
+ type: 'post',
7
+ data: {'action': 'opt_checking'},
8
+ dataType: 'json',
9
+ beforeSend: function() {
10
+ },
11
+ success: function(response) {
12
+ console.log(response);
13
+ ret = response;
14
+ for (sid in response) {
15
+ jQuery('#' + sid).removeClass('hz-loading').text(response[sid][0] + '/' + response[sid][1]);
16
+ }
17
+ console.log(typeof (call_back));
18
+ if (typeof (call_back) == "function") {
19
+ call_back(response);
20
+ }
21
+ },
22
+ error: function() {
23
+
24
+ }
25
+
26
+ });
27
+
28
+ return ret;
29
+ }
30
+
31
+
32
+ function drawChart(metacontent,notmetacontent, imagemeta,notimagemeta, imageoptimi,notimageoptimi) {
33
+
34
+ var doughnutData = [
35
+ {
36
+ value: metacontent,
37
+ color: "rgba(92,155,213,1)",
38
+ highlight: "#5c9bd5",
39
+ label: "Content Meta- Completed"
40
+ },
41
+ {
42
+ value: notmetacontent,
43
+ color: "#CECECE",
44
+ label: "Content Meta- Imcompleted"
45
+ },
46
+ {
47
+ value: imagemeta,
48
+ color: "rgba(241,145,79,1)",
49
+ highlight: "#ed7d31",
50
+ label: "Image Meta - Completed"
51
+ },
52
+ {
53
+ value: notimagemeta,
54
+ color: "#CECECE",
55
+ label: "Image Meta - Imcompleted"
56
+ },
57
+
58
+ {
59
+ value: imageoptimi,
60
+ color: "rgba(70,191,189,1)",
61
+ highlight: "#46BFBD",
62
+ label: "Image Optimized"
63
+ },
64
+ {
65
+ value: notimageoptimi,
66
+ color: "#CECECE",
67
+ label: "Image not Optimized"
68
+ }
69
+
70
+
71
+ ];
72
+
73
+ console.log(doughnutData);
74
+ var ctx = document.getElementById("chart-area").getContext("2d");
75
+ window.myDoughnut = new Chart(ctx).Doughnut(doughnutData, {
76
+ responsive: true,
77
+ tooltipTitleFontSize: 13,
78
+ tooltipXOffset: 5,
79
+ tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= ((value*3) %1 !=0)?(value*3).toFixed(2):(value*3) %> %",
80
+ percentageInnerCutout: 70,
81
+ scaleShowLabels: true,
82
+ segmentShowStroke : false,
83
+
84
+ });
85
+
86
+
87
+ }
js/metaseo_admin.js ADDED
@@ -0,0 +1,784 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ * To change this license header, choose License Headers in Project Properties.
3
+ * To change this template file, choose Tools | Templates
4
+ * and open the template in the editor.
5
+ */
6
+ var title_max_len = 69;
7
+ var desc_max_len = 156;
8
+ var metaseoValueHolder = {};
9
+ var iUnchecked = 0;
10
+
11
+ function metaseo_clean(str) {
12
+ if (str == '' || str == undefined)
13
+ return '';
14
+ try {
15
+ str = jQuery('<div/>').html(str).text();
16
+ str = str.replace(/<\/?[^>]+>/gi, '');
17
+ str = str.replace(/\[(.+?)\](.+?\[\/\\1\])?/g, '');
18
+ } catch (e) {
19
+ }
20
+
21
+ return str;
22
+ }
23
+
24
+ var oldTitleValues = {};
25
+ var oldDescValues = {};
26
+ var metaContentChangeWait;
27
+ function metaseo_updateTitle(metatitle_id, needToSave, updateSnippet) {
28
+ var title = jQuery.trim(metaseo_clean(jQuery('#' + metatitle_id).val()));
29
+ var postid = metatitle_id.replace('metaseo-metatitle-', '');
30
+ var counter_id = 'metaseo-metatitle-len' + postid;
31
+ jQuery('#' + counter_id).text(title_max_len-title.length);
32
+ if (title.length >= title_max_len) {
33
+ jQuery('#' + counter_id).addClass('word-exceed');//#FEFB04
34
+ }
35
+ else {
36
+ jQuery('#' + counter_id).removeClass('word-exceed');
37
+ }
38
+
39
+ if(title.length > title_max_len){
40
+ jQuery('#snippet_title' + postid).empty().text(title.substr(0, title_max_len));
41
+ }
42
+
43
+ if(typeof updateSnippet == "undefined" || updateSnippet !== false) {
44
+ jQuery('#snippet_title' + postid).text(title.substr(0, title_max_len) );
45
+ }
46
+
47
+ if (needToSave === true && oldTitleValues[postid] != title ) {
48
+
49
+ clearTimeout(metaContentChangeWait);
50
+ metaContentChangeWait = setTimeout(function() {
51
+ saveMetaContentChanges('metatitle', postid, title);
52
+ }, 1000);
53
+ }
54
+
55
+ //Push the new value into the array
56
+ oldTitleValues[postid] = title;
57
+ }
58
+
59
+ function metaseo_updateDesc(metadesc_id, needToSave) {
60
+ var desc = jQuery.trim(metaseo_clean(jQuery('#' + metadesc_id).val()));
61
+ var postid = metadesc_id.replace('metaseo-metadesc-', '');
62
+ var counter_id = 'metaseo-metadesc-len' + postid;
63
+ jQuery('#' + counter_id).text(desc_max_len-desc.length);
64
+
65
+ if (desc.length >= desc_max_len) {
66
+ jQuery('#' + counter_id).addClass('word-exceed');
67
+ }
68
+ else {
69
+ jQuery('#' + counter_id).removeClass('word-exceed');
70
+ }
71
+
72
+ jQuery('#snippet_desc' + postid).text(desc.substr(0, desc_max_len) );
73
+
74
+ if (needToSave === true && oldDescValues[postid] != desc) {
75
+
76
+ clearTimeout(metaContentChangeWait);
77
+ metaContentChangeWait = setTimeout(function() {
78
+ saveMetaContentChanges('metadesc', postid, desc);
79
+ }, 1000);
80
+ }
81
+
82
+ //Push the new value into the array
83
+ oldDescValues[postid] = desc;
84
+ }
85
+
86
+ var autosaveNotification;
87
+ function saveMetaContentChanges(metakey, postid, data) {
88
+
89
+ var postData = {
90
+ 'action': 'updateContentMeta',
91
+ 'metakey': metakey,
92
+ 'postid': postid,
93
+ 'value': data
94
+ };
95
+ // We can also pass the url value separately from ajaxurl for front end AJAX implementations
96
+ jQuery.post(myAjax.ajax_url, postData, function(response) {
97
+ result = jQuery.parseJSON(response);
98
+
99
+ if (result.updated == true) {
100
+ autosaveNotification = setTimeout(function() {
101
+ jQuery('#savedInfo' + postid).text(result.msg);
102
+ jQuery('#savedInfo' + postid).fadeIn(200).delay(2000).fadeOut(1000);
103
+ }, 1000);
104
+ } else {
105
+ alert(result.msg);
106
+ }
107
+
108
+ });
109
+
110
+
111
+ }
112
+
113
+ function checkspecial(element_id){
114
+ var element = jQuery(element_id);
115
+ var meta_type = element.data('meta-type');
116
+
117
+ if(meta_type=='change_image_name'){
118
+ var str = (element.val());
119
+ if( /^[\w\d\-\s+_.$]*$/.test( str ) == false ) {
120
+ clearTimeout(metaChangeWait);
121
+ return false;
122
+ }else{
123
+ return true;
124
+ }
125
+ }else{
126
+ return true;
127
+ }
128
+ }
129
+
130
+ var metaChangeWait;
131
+ function metaseo_update(element_id) {
132
+ //metaseo-img-alt-4
133
+ var element = jQuery(element_id);
134
+ var post_id = element.data('post-id');
135
+ var meta_type = element.data('meta-type');
136
+ var meta_value = element.val();
137
+
138
+ clearTimeout(metaChangeWait);
139
+ metaChangeWait = setTimeout(function() {
140
+ if(saveChanges(element_id, post_id, meta_type, meta_value)){
141
+ //jQuery(element_id).parent().find('span.meta-update').removeClass('update-loader');
142
+
143
+ if(meta_type === 'change_image_name'){
144
+ jQuery('a.img-resize[data-post-id="'+ post_id +'"]').data('img-name', meta_value);
145
+ }
146
+
147
+ }
148
+ }, 1000);
149
+ }
150
+
151
+
152
+
153
+ function saveChanges(element_id, post_id, meta_type, meta_value) {
154
+
155
+ var element = jQuery(element_id);
156
+ var savedInfo = element.parent().find('span.saved-info');
157
+ if(savedInfo.length < 1) { savedInfo = element.closest('td').find('span.saved-info'); }
158
+ var updated = false;
159
+ var postData = {
160
+ 'action': 'updateMeta',
161
+ 'post_id': post_id,
162
+ 'meta_type': meta_type,
163
+ 'meta_value':meta_value,
164
+ 'addition' : {
165
+ 'meta_key' : element.data('meta-key'),
166
+ 'meta_type': element.data('meta-type'),
167
+ 'meta_value' : element.val(),
168
+ 'meta_order' : element.data('meta-order'),
169
+ 'img_post_id' : element.data('img-post-id'),
170
+ 'post_id' : element.data('post-id'),
171
+ }
172
+ };
173
+
174
+ // We can also pass the url value separately from ajaxurl for front end AJAX implementations
175
+ jQuery.ajax({
176
+ url: myAjax.ajax_url,
177
+ async: false,
178
+ type: 'post',
179
+ data: postData,
180
+ dataType: 'json',
181
+ beforeSend: function(){
182
+ savedInfo.empty().append('<span style="position:absolute" class="meta-update"></span>');
183
+ element.parent().find('span.meta-update').addClass('update-loader').fadeIn(300);
184
+ },
185
+ success: function(response){
186
+ if(response == 0){
187
+ saveChanges(element_id, post_id, meta_type, meta_value);
188
+ }
189
+
190
+ metaChangeWait = -1 ;
191
+ updated = response.updated;
192
+
193
+ if (updated == true) {
194
+ autosaveNotification = setTimeout(function() {
195
+ element.parent().find('span.meta-update').removeClass('update-loader');
196
+ savedInfo.removeClass('metaseo-msg-warning').addClass('metaseo-msg-success')
197
+ .text(response.msg).fadeIn(200);
198
+
199
+ setTimeout(function(){
200
+ savedInfo.empty().append('<span class="meta-update" style="position:absolute"></span>');
201
+ }, 3000);
202
+
203
+ }, 200);
204
+
205
+ //update image's data-name attribute
206
+ if(typeof element.data('extension') != 'undefined'){
207
+ jQuery('[data-img-post-id="'+element.data('post-id')+'"]').data('name', element.val() + element.data('extension'));
208
+ }
209
+ //Scan post and update post_meta
210
+ var img = jQuery('[data-img-post-id="'+ postData['addition']['img_post_id'] +'"]');
211
+
212
+ if(img.length > 0){
213
+ _metaSeoScanImages(
214
+ [
215
+ {
216
+ 'name':img.data('name'),
217
+ 'img_post_id':postData['addition']['img_post_id']
218
+ }
219
+
220
+ ]);
221
+ }
222
+
223
+ } else {
224
+ element.val(response.iname);
225
+ savedInfo.removeClass('metaseo-msg-success').addClass('metaseo-msg-warning')
226
+ .text(response.msg).fadeIn(200).delay(2000).fadeOut(200);
227
+ }
228
+ },
229
+ error: function(){
230
+
231
+ }
232
+ });
233
+
234
+ return updated;
235
+ }
236
+
237
+ //Scan all posts to find a group of images in their content
238
+ function metaSeoScanImages(){
239
+ var imgs = [];
240
+ jQuery('.metaseo-image').each(function(i){
241
+ if(jQuery(this).data('name') != ''){
242
+ imgs[i] = {
243
+ 'name':jQuery(this).data('name'),
244
+ 'img_post_id':jQuery(this).data('img-post-id')
245
+ };
246
+ }
247
+ });
248
+
249
+ _metaSeoScanImages(imgs);
250
+
251
+ }
252
+
253
+ function _metaSeoScanImages(imgs){
254
+ if(imgs.length < 1){
255
+ //alert('No images choosen for scanning, please check again!');
256
+ return false;
257
+ }
258
+
259
+ jQuery.ajax({
260
+ url: myAjax.ajax_url,
261
+ type: 'post',
262
+ data: {'action':'scanPosts', 'imgs':imgs},
263
+ dataType: 'json',
264
+ beforeSend: function(){},
265
+ success: function(response){
266
+ if(response == 0) { _metaSeoScanImages(imgs); }
267
+ //clog(imgs);
268
+ if(response.success === true){
269
+ //Clear content holder first
270
+ if(imgs.length === 1){
271
+ jQuery('#opt-info-'+imgs[0]['img_post_id']).removeClass('opt-info-warning').empty();
272
+ }
273
+
274
+ //id is refered to image post id
275
+ for(var iID in response.msg){
276
+ //Change css position property of td tag to default
277
+ jQuery('#opt-info-'+iID).parent().css('position', 'static');
278
+ jQuery('#opt-info-'+iID).append('<p class="btn-wrapper"></p>');
279
+
280
+ for(var msgType in response.msg[iID]){
281
+ if(response.msg[iID][msgType]['warning'] == true
282
+ && !jQuery('#opt-info-'+iID).hasClass('opt-info-warning')){
283
+ jQuery('#opt-info-'+iID).addClass('opt-info-warning');
284
+ }
285
+
286
+ jQuery('#opt-info-'+iID).find('p.btn-wrapper').append(response.msg[iID][msgType]['button']);
287
+ if(typeof response.msg[iID][msgType]['msg'] != 'object'){
288
+ var hlight = !response.msg[iID][msgType]['warning'] ? 'metaseo-msg-success' : '';
289
+ jQuery('#opt-info-'+iID).prepend('<p class="'+hlight+'">'+response.msg[iID][msgType]['msg']+'</p>');
290
+ }
291
+ else{
292
+ for(var k in response.msg[iID][msgType]['msg']){
293
+ jQuery('#opt-info-'+iID).prepend('<p>'+response.msg[iID][msgType]['msg'][k]+'</p>');
294
+ }
295
+ }
296
+ }
297
+
298
+ }
299
+
300
+ jQuery('span.metaseo-loading').hide();
301
+ jQuery('.opt-info-warning').fadeIn(200);
302
+
303
+ //
304
+ jQuery('input.metaseo-checkin').each(function(i, input){
305
+ uncheck(input);
306
+ });
307
+ }
308
+ },
309
+ error: function(){
310
+ alert('Errors occured while scanning posts for optimization');
311
+ }
312
+ });
313
+ }
314
+
315
+ //To fix meta of a specified image
316
+ function metaseo_fix_meta(that){
317
+ var $this = jQuery(that);
318
+
319
+ if(checkspecial(that) === true){
320
+
321
+ if(that.jquery === undefined){
322
+ $this.bind('input propertychange', function(){
323
+ metaseo_update(that);
324
+ });
325
+ }
326
+ else{
327
+ metaseo_update(that);
328
+ }
329
+
330
+ }
331
+ }
332
+
333
+ function add_meta_default(that){
334
+ var $this = jQuery(that);
335
+ var input = $this.parent().find('input');
336
+ var id = input.attr('id');
337
+
338
+ input.val($this.data('default-value'));
339
+ metaseo_fix_meta(input);
340
+ }
341
+ //--------------------------------
342
+ //Optimize a single post
343
+ function optimize_imgs(element){
344
+ var $this = jQuery(element);
345
+ var post_id = $this.data('post-id');
346
+ var img_post_id = $this.data('img-post-id');
347
+ var checkin = jQuery('.checkin-'+post_id);
348
+ var img_exclude = [];
349
+ var not_checked_counter = 0;
350
+ var updated = false;
351
+
352
+ var j=0;
353
+ checkin.each(function(i,el){
354
+ if(!(jQuery(el).is(':checked'))){
355
+ not_checked_counter++;
356
+ if(jQuery(el).val() != '' || jQuery(el).val() != 'undefined'){
357
+ img_exclude[j] = parseInt(jQuery(el).val());
358
+ j++;
359
+ }
360
+ }
361
+ });
362
+
363
+ if(checkin.length <= not_checked_counter){
364
+ //alert('No images has choosen. \\nPlease click on the checkbox in what image you want to replace!');
365
+ return false;
366
+ }
367
+
368
+ if(!post_id && !img_post_id){
369
+ alert('Cant do the optimization because of missing image ID.\\nPlease check again!');
370
+ }
371
+ else{
372
+ jQuery.ajax({
373
+ url:myAjax.ajax_url,
374
+ async: false,
375
+ data:{
376
+ 'action': 'optimize_imgs',
377
+ 'post_id' : post_id,
378
+ 'img_post_id' : img_post_id,
379
+ 'img_exclude' : img_exclude
380
+ },
381
+ dataType: 'json',
382
+ type: 'post',
383
+ beforeSend: function(){
384
+ $this.parent().find('span.spinner').show();
385
+ },
386
+ success: function(response){
387
+ if(response == 0){ optimize_imgs(element); }
388
+
389
+ if(response.success){
390
+ updated = true;
391
+
392
+ checkin.each(function(i,e){
393
+ if(jQuery.inArray(parseInt(jQuery(this).val()), img_exclude) == -1){
394
+
395
+ var img_choosen = jQuery(this).parent();
396
+ jQuery(this).remove();
397
+
398
+ img_choosen.empty().append('<span class="metaseo-checked"></span>');
399
+ img_choosen.parent().find('p.metaseo-msg').removeClass('msg-error').addClass('msg-success').empty().text(response.msg).fadeIn(200);
400
+ setTimeout(function(){
401
+ img_choosen.find('p.metaseo-msg').fadeOut(300);
402
+ }, 5000);
403
+
404
+ }
405
+ });
406
+
407
+ var checked = jQuery('.checkin-'+post_id);
408
+ if(checked.length == 0){
409
+ $this.addClass('disabled');
410
+ }
411
+
412
+ $this.parent().find('span.spinner').fadeOut(300);
413
+ //Disable Replace all button if all image were resized
414
+ var metaseo_checkin = jQuery('.metaseo-checkin');
415
+
416
+ if(metaseo_checkin.length == 0){
417
+ jQuery('#metaseo-replace-all').addClass('disabled');
418
+ }
419
+ //Scan post and update post_meta
420
+ var img = jQuery('[data-img-post-id="'+ img_post_id +'"]');
421
+ _metaSeoScanImages([{'name':img.data('name'), 'img_post_id':img_post_id}]);
422
+
423
+ }
424
+ else{
425
+ $this.parent().find('span.spinner').hide();
426
+ $this.parent().find('p.metaseo-msg').removeClass('msg-success').addClass('msg-error');
427
+ }
428
+
429
+ },
430
+ error: function(){
431
+
432
+ }
433
+ });
434
+ }
435
+
436
+ img_exclude = [];
437
+ return updated;
438
+ }
439
+
440
+ //Optimize all posts in list displayed
441
+ function optimize_imgs_group(that){
442
+ jQuery('a.metaseo-optimize').each(function(i, el){
443
+ if(i == 0){
444
+ jQuery(that).parent().find('span.spinner').show();
445
+ }
446
+
447
+ jQuery(this).click();
448
+
449
+ if(i == (jQuery(el).length - 1)){
450
+ jQuery(that).parent().find('span.spinner').hide();
451
+ }
452
+ });
453
+
454
+ }
455
+
456
+ function uncheck(that){
457
+ var $this = jQuery(that);
458
+ var post_id = that.className.substr(that.className.lastIndexOf('-')+1);
459
+ var checked = jQuery('.checkin-'+post_id);
460
+ var not_checked_counter = 0;
461
+
462
+ checked.each(function(i,e){
463
+ if(!(jQuery(this).is(':checked'))){
464
+ not_checked_counter++;
465
+ }
466
+ });
467
+
468
+ //Toggle disable Replace button if all images in a post were resized
469
+ if(not_checked_counter >= checked.length){
470
+ jQuery('a.metaseo-optimize[data-post-id="'+post_id+'"]').addClass('disabled');
471
+ }
472
+ else{
473
+ jQuery('a.metaseo-optimize[data-post-id="'+post_id+'"]').removeClass('disabled');
474
+ }
475
+
476
+ //Toggle disable Replace all button if all images in posts were resized
477
+ var replaceBtns = jQuery('.metaseo-optimize');
478
+ var disable = true;
479
+ replaceBtns.each(function(i, btn){
480
+ if(!jQuery(btn).hasClass('disabled')){
481
+ disable = false;
482
+ return;
483
+ }
484
+ });
485
+
486
+ if(disable === true){
487
+ jQuery('#metaseo-replace-all').addClass('disabled');
488
+ }
489
+ else{
490
+ jQuery('#metaseo-replace-all').removeClass('disabled');
491
+ }
492
+ }
493
+
494
+ //Show posts list for resizing or update meta info of an image with specified id
495
+ function showPostsList(element){
496
+ var that = jQuery(element);
497
+ var data = {
498
+ 'action': 'load_posts',
499
+ 'img_name': that.data('img-name'),
500
+ 'post_id': that.data('post-id'),
501
+ 'opt_key': that.data('opt-key')
502
+ };
503
+
504
+ if(that.data('img-name') != ''){
505
+ jQuery.ajax({
506
+ url: myAjax.ajax_url,
507
+ type: 'post',
508
+ dataType: 'html',
509
+ data: data,
510
+ beforeSend:function(){
511
+ that.find('.spinner-light').show();
512
+ },
513
+ success: function(response){
514
+ if(response == 0){ showPostsList(element); }
515
+ that.parent().find('.spinner-light').hide();
516
+ that.closest('td.col_image_info').find('div.popup > .popup-content').empty().html(response).fadeIn(300);
517
+
518
+ //jQuery('.img-choosen').css({'right' : -(jQuery('.metaseo-action').outerWidth()/2 + 20), 'bottom' : '3px'});
519
+
520
+ //to set background-color of popup-header to like adminmenu active
521
+ var metaseo_bg = jQuery('#adminmenu li.wp-has-current-submenu a.wp-has-current-submenu').css('background-color');
522
+
523
+ if (metaseo_bg !== 'undified') {
524
+ jQuery('.popup-content .content-header').css({'background-color' : metaseo_bg, 'color': '#FFF'});
525
+ jQuery('span.popup-close').css({'color' : '#FFF'});
526
+ }
527
+
528
+ that.showPopup(that);
529
+ }
530
+ });
531
+ }
532
+ else{
533
+ alert('Something went wrong, please check Image name if it\'s empty before click Resize button');
534
+ }
535
+ }
536
+
537
+ //Import meta data from other plugin into Wp Meta Seo
538
+ function importMetaData(that, event){
539
+ var plugin = that.id;
540
+ var element = jQuery('#'+that.id);
541
+
542
+ event.preventDefault();
543
+ if(that.id === '_aio_' || that.id === '_yoast_'){
544
+ jQuery.ajax({
545
+ url: myAjax.ajax_url,
546
+ type: 'post',
547
+ data: {'action': 'import_meta_data', 'plugin' : that.id},
548
+ dataType: 'json',
549
+ beforeSend: function(){
550
+ element.find('span.spinner-light').show();
551
+ },
552
+ success: function(response){
553
+ if(response.success == true){
554
+ element.find('span.spinner-light').fadeOut(500);
555
+ jQuery('.metaseo-import-wrn').closest('.error').fadeOut(1500);
556
+ console.log(location.href);
557
+ //Refresh the page to see al changed after import Yoast or AIO data into MetaSEO
558
+ if( location.href.search('page=metaseo_content_meta') != -1 ){
559
+ location.reload();
560
+ }
561
+ //setTimeout(function(){
562
+ //jQuery('.mseo-import-action').closest('.error').remove();
563
+ //},5000);
564
+
565
+ }
566
+ },
567
+ error: function(){
568
+ alert('Something went wrong in import processing!');
569
+ }
570
+
571
+ });
572
+ }
573
+ }
574
+
575
+ //Update once input changed and blur
576
+ function updateInputBlur(that){
577
+ var element = jQuery(that);
578
+ var post_id = element.data('post-id');
579
+ var meta_type = element.data('meta-type');
580
+ var meta_value = element.val();
581
+
582
+ if(typeof metaChangeWait != "undefined" && (metaChangeWait != -1) ){
583
+ clearTimeout(metaChangeWait);
584
+ if(saveChanges(that, post_id, meta_type, meta_value)){
585
+ if(meta_type === 'change_image_name'){
586
+ jQuery('a.img-resize[data-post-id="'+ post_id +'"]').data('img-name', meta_value);
587
+ }
588
+ }
589
+ }
590
+ }
591
+
592
+ function checkeyCode(event,that){
593
+ if(event.which == 13 || event.keyCode == 13){
594
+ return false;
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Check if a name is existed
600
+ */
601
+ function is_existed(iname){
602
+ jQuery.each(metaseoValueHolder, function(i, iName){
603
+ for(var id in iName){
604
+ if(typeof iName[id+'_prev'] != 'undefined' && iname == iName[id+'_prev']){
605
+ return true;
606
+ }
607
+ }
608
+ });
609
+ }
610
+
611
+ /**
612
+ * Check if a image name is valid or not
613
+ */
614
+ function validateiName(iname){
615
+ var is_only_spaces = iname.length > 0 ? true : false;
616
+ iname = iname.trim();
617
+ var msg = '';
618
+
619
+ if( iname.length < 1 ){
620
+ msg = !is_only_spaces ? 'Should not be empty' : 'Should not only spaces';
621
+ return {msg: msg, name: ''};
622
+ }
623
+
624
+ return {msg: '', name: iname};
625
+ }
626
+
627
+ jQuery(document).ready(function($) {
628
+
629
+ //Cursor changes on any ajax start and end
630
+ //Thanks to iambriansreed from stacoverflow.com
631
+ $('body').ajaxStart(function() {
632
+ $(this).css({'cursor':'wait'});
633
+ }).ajaxStop(function() {
634
+ $(this).css({'cursor':'default'});
635
+ });
636
+
637
+ $('span.pagination-links a.disabled').click(function(e) {
638
+ e.preventDefault();
639
+ });
640
+
641
+ $('.metaseo_imgs_per_page').bind('input propertychange', function(){
642
+ var perpage = $(this).val();
643
+ $('.metaseo_imgs_per_page').each(function(i,e){
644
+ if($(e).val() != perpage){
645
+ $(e).val(perpage);
646
+ }
647
+ });
648
+ });
649
+
650
+ $('.metaseo-filter').bind('change', function(){
651
+ var value = $(this).val();
652
+ $('.metaseo-filter').each(function(i,e){
653
+ if($(e).val() != value){
654
+ $(e).val(value);
655
+ }
656
+ });
657
+ });
658
+
659
+ $('.metaseo-img-name').bind('input propertychange', function() {
660
+ var savedInfo = $(this).parent().find('span.saved-info');
661
+ var iname = validateiName( $(this).val() );
662
+ var msg = iname.msg;
663
+
664
+ if( iname.name.length > 0 ){
665
+ if(!checkspecial(this)){
666
+ msg = 'Should not special char';
667
+ }
668
+ else{
669
+ metaseo_update(this);
670
+ }
671
+ }
672
+
673
+ if(msg.length > 0){
674
+ //Set this value to metaseoValueHolder
675
+ metaseoValueHolder[this.id] = iname.name.substr(0, iname.name.length - 1);
676
+
677
+ savedInfo.removeClass('metaseo-msg-success')
678
+ .addClass('metaseo-msg-warning').empty().text(msg);
679
+ }
680
+ });
681
+
682
+ $('.metaseo-img-meta').bind('input propertychange', function(){
683
+ var savedInfo = $(this).parent().find('span.saved-info');
684
+ var metaseoValue = $(this).val();
685
+
686
+ if(checkspecial(this) === true){
687
+ if($(this).hasClass('metaseo-img-name')){
688
+ if(metaseoValue.trim().length > 0
689
+ && typeof metaseoValueHolder[this.id] != 'undefined'
690
+ && metaseoValueHolder[this.id] !== metaseoValue) {
691
+ metaseo_update(this);
692
+ }
693
+ }
694
+ else{
695
+ metaseo_update(this);
696
+ }
697
+
698
+ }
699
+ });
700
+
701
+ $('.metaseo-img-meta').each(function(i, element){
702
+ if($(this).hasClass('metaseo-img-name')){
703
+ metaseoValueHolder[this.id+'_prev'] = jQuery(this).val();
704
+ }
705
+ $(element).bind('keydown', function(event){
706
+ if(event.which == 13 || event.keyCode == 13){
707
+ return false;
708
+ }
709
+ });
710
+ });
711
+
712
+ $('.metaseo-img-meta').blur(function() {
713
+ if(jQuery(this).val() == ''){
714
+ jQuery(this).val(metaseoValueHolder[this.id+'_prev']);
715
+ $(this).parent().find('span.saved-info').empty().append('<span style="position:absolute" class="meta-update"></span>');
716
+ }
717
+ if(checkspecial(this) === true){
718
+ updateInputBlur(this);
719
+ }
720
+ });
721
+
722
+ $('.dissmiss-import').bind('click', function(e){
723
+ e.preventDefault();
724
+ $(this).closest('.error').fadeOut(1000);
725
+ setTimeout(function(){
726
+ $(this).closest('.error').remove();
727
+ },5000);
728
+
729
+ var plugin = $(this).parent().find('a.button').attr('id');
730
+
731
+ if(plugin === '_aio_' || plugin === '_yoast_'){
732
+ $.ajax({
733
+ url: myAjax.ajax_url,
734
+ type: 'post',
735
+ data: {'action': 'dismiss_import_meta', 'plugin' : plugin},
736
+ dataType: 'json',
737
+ beforeSend: function(){
738
+
739
+ },
740
+ success: function(response){
741
+ if(response.success !== true){
742
+ alert('Dismiss failed!');
743
+ }
744
+ }
745
+ });
746
+ }
747
+ });
748
+ //----------------------------------------------------------
749
+ //Pop-up declaration
750
+ $.fn.absoluteCenter = function() {
751
+ this.each(function() {
752
+ var top = -($(this).outerHeight() / 2) + 'px';
753
+ var left = -($(this).outerWidth() / 2) + 'px';
754
+ $(this).css({'position': 'fixed', 'top': $('div.wrap').offset().top, 'left': $('div.wrap').offset().left, 'right': '25px', 'bottom': '10px'});
755
+
756
+ return this;
757
+ });
758
+ };
759
+
760
+ $.fn.showPopup = function(that){
761
+ var bg = $('div.popup-bg');
762
+ var obj = that.closest('.col_image_info').find('div.popup');
763
+ var btnClose = obj.find('.popup-close');
764
+ bg.animate({opacity: 0.2}, 0).fadeIn(200);
765
+ //obj.fadeIn(200).draggable({cursor: 'move', handle: '.popup-header'}).absoluteCenter();
766
+ obj.fadeIn(200).absoluteCenter();
767
+ btnClose.click(function() {
768
+ bg.fadeOut(100);
769
+ obj.fadeOut(100).find('div.popup-content').empty();
770
+ });
771
+ bg.click(function() {
772
+ btnClose.click();
773
+ });
774
+ $(document).keydown(function(e) {
775
+ if (e.keyCode == 27) {
776
+ btnClose.click();
777
+ }
778
+ });
779
+ return false;
780
+ };
781
+ $('a.show-popup').bind('click', function() {
782
+ $(this).showPopup($(this));
783
+ });
784
+ });
js/pop-up.js ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ jQuery(document).ready(function() {
2
+ (function($) {
3
+ $.fn.absoluteCenter = function() {
4
+ this.each(function() {
5
+ var top = -($(this).outerHeight() / 2) + 'px';
6
+ var left = -($(this).outerWidth() / 2) + 'px';
7
+ $(this).css({'position': 'absolute', 'position':'fixed', 'margin-top': top, 'margin-left': left, 'top': '40%', 'left': '50%'});
8
+ return this;
9
+ });
10
+ }
11
+ })(jQuery);
12
+
13
+ $('a.show-popup').click(function() {
14
+ var bg = $('div.popup-bg');
15
+ var obj = $(this).parent().find('div.popup');
16
+ var btnClose = obj.find('.popup-close');
17
+ bg.animate({opacity: 0.2}, 0).fadeIn(200);
18
+ obj.fadeIn(200).draggable({cursor: 'move', handle: '.popup-header'}).absoluteCenter();
19
+ btnClose.click(function() {
20
+ bg.fadeOut(100);
21
+ obj.fadeOut(100);
22
+ });
23
+ bg.click(function() {
24
+ btnClose.click();
25
+ });
26
+ $(document).keydown(function(e) {
27
+ if (e.keyCode == 27) {
28
+ btnClose.click();
29
+ n
30
+ }
31
+ });
32
+ return false;
33
+ });
34
+ });
readme.txt ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ === WP Meta SEO ===
2
+ Contributors: JoomUnited
3
+ Tags: bing, description, google, google webmaster tools, keywords, meta, meta description, meta keywords, robots meta, search engine optimization, seo, Webmaster Tools, wordpress seo, yahoo, image optimization, image resize, photo, custom post, post, posts, page
4
+ Requires at least: 4.0
5
+ Tested up to: 4.1.0
6
+ Stable tag: trunk
7
+ License: GPLv2 or later
8
+ License URI: http://www.gnu.org/licenses/gpl-2.0.html
9
+
10
+ WP Meta SEO give you the control over all your content and image meta. In one view you will be able to add some meta, alt tag and resize photos.
11
+
12
+
13
+ == Description ==
14
+
15
+ WP meta SEO got 2 major functionnalities: bulk edit all website meta with intuitive interface and optimize wrong image size.
16
+
17
+ It was so annoying before and almost nobody was able to edit all meta content and image meta.
18
+ WP Meta SEO is going to list all articles, pages, custom post and all images. Update meta and image size and everything is AJAX saved.
19
+ You can now optimize the size of your images in articles. A bunch of people reduce the size of the pictures handling the border and resizing in html/css.
20
+
21
+ **More details here:** http://www.joomunited.com/wordpress-products/wp-meta-seo
22
+
23
+ = Video demo: =
24
+ [vimeo https://vimeo.com/113695156]
25
+
26
+
27
+ **Features**
28
+
29
+ * Reduce page weight by fix image size
30
+ * All alt tags edition on single view
31
+ * See all your snippet in one glance
32
+ * Image name, title, description and legend
33
+ * Automatic AJAX saving
34
+ * No additional complex params
35
+ * Custom type mate edition
36
+ * Yoast SEO import & sync
37
+ * Alexa world rank
38
+ * Meta content filtering
39
+ * Optimise images in all your post and pages
40
+ * Live Google snippet preview
41
+ * Edit images file name in single view
42
+ * All in one SEO importer and sync Filter content by post type: post, page, custom
43
+ * Image date filtering
44
+ * Content meta length limit (Google + Yahoo + Bing)
45
+ * SEO problem warning
46
+ * Bulk image resizing
47
+ * One click image title copy
48
+
49
+ = Support =
50
+
51
+ A PDF support documant is provided and a dedicated support can be provided additionnaly with a membership.
52
+ **More details here:** http://www.joomunited.com/wordpress-products/wp-meta-seo
53
+
54
+ == Installation ==
55
+
56
+ WordPress installation is fully supported.
57
+ Once the plugin is installed, just load the main plugin view to configure all meta and image optimization
58
+
59
+
60
+ == Screenshots ==
61
+
62
+ 1. Main dashboad of the plugin with missing optimization and alexa rank
63
+ 1. Bulk content meta edition
64
+ 1. Content lenght is optimized for Google, Bing and Yahoo
65
+ 1. Image resized in HTML (with handles) is automatically detected and can be resized
66
+ 1. SEO Yoast and AIO SEO plugin and detected and data sync
67
+ 1. Detail and alert on the element that you can optimize
68
+ 1. Image replace confirmation
69
+ 1. Title and filename image edition without and broken link
70
+
71
+
72
+ == Revisions ==
73
+
74
+ * 1.0.0 Initial (free) release
75
+
76
+ == Changelog ==
77
+
78
+ = 1.0.0 =
79
+
80
+ * Initial release
81
+
82
+
83
+ == Upgrade Notice ==
84
+
85
+ Install new version over the old one
86
+
87
+
88
+ == Requirements ==
89
+
90
+ PHP 5.3+
wp-meta-seo.php ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Plugin Name: WP Meta SEO
5
+ * Plugin URI: http://www.joomunited.com/wordpress-products/wp-meta-seo
6
+ * Description: WP Meta SEO is a plugin for WordPress to fill meta for content, images and main SEO info in a single view.
7
+ * Version: 1.0.0
8
+ * Author: JoomUnited
9
+ * Author URI: http://www.joomunited.com
10
+ * License: GPL2
11
+ */
12
+ /**
13
+ * @copyright 2014 Joomunited ( email : contact _at_ joomunited.com )
14
+ *
15
+ * Original development of this plugin was kindly funded by Joomunited
16
+ *
17
+ * This program is free software; you can redistribute it and/or modify
18
+ * it under the terms of the GNU General Public License as published by
19
+ * the Free Software Foundation; either version 2 of the License, or
20
+ * (at your option) any later version.
21
+ *
22
+ * This program is distributed in the hope that it will be useful,
23
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
24
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25
+ * GNU General Public License for more details.
26
+ *
27
+ * You should have received a copy of the GNU General Public License
28
+ * along with this program; if not, write to the Free Software
29
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
30
+ */
31
+ # error_reporting(E_ALL);
32
+ // Make sure we don't expose any info if called directly
33
+ if (!function_exists('add_action')) {
34
+ echo 'Hi there! I\'m just a plugin, not much I can do when called directly.';
35
+ exit;
36
+ }
37
+
38
+ if (!defined('WPMETASEO_MINIMUM_WP_VERSION'))
39
+ define('WPMETASEO_MINIMUM_WP_VERSION', '3.1');
40
+ if (!defined('WPMETASEO_PLUGIN_URL'))
41
+ define('WPMETASEO_PLUGIN_URL', plugin_dir_url(__FILE__));
42
+ if (!defined('WPMETASEO_PLUGIN_DIR'))
43
+ define('WPMETASEO_PLUGIN_DIR', plugin_dir_path(__FILE__));
44
+ if (!defined('URL'))
45
+ define('URL', get_site_url());
46
+
47
+ register_activation_hook(__FILE__, array('WpMetaSeo', 'plugin_activation'));
48
+ register_deactivation_hook(__FILE__, array('WpMetaSeo', 'plugin_deactivation'));
49
+
50
+
51
+ require_once( WPMETASEO_PLUGIN_DIR . 'inc/class.wp-metaseo.php' );
52
+ add_action('init', array('WpMetaSeo', 'init'));
53
+
54
+ if (is_admin()) {
55
+ require_once( WPMETASEO_PLUGIN_DIR . 'inc/class.metaseo-content-list-table.php' );
56
+ require_once( WPMETASEO_PLUGIN_DIR . 'inc/class.metaseo-image-list-table.php' );
57
+ require_once( WPMETASEO_PLUGIN_DIR . 'inc/class.metaseo-dashboard.php' );
58
+ require_once( WPMETASEO_PLUGIN_DIR . 'inc/class.metaseo-admin.php' );
59
+
60
+ $GLOBALS['metaseo_admin'] = new MetaSeo_Admin;
61
+
62
+ add_filter('wp_prepare_attachment_for_js', array('MetaSeo_Image_List_Table', 'add_more_attachment_sizes_js'), 10, 2);
63
+ add_filter('image_size_names_choose', array('MetaSeo_Image_List_Table', 'add_more_attachment_sizes_choose'), 10, 1);
64
+ } else {
65
+ /******** Check again and modify title, meta title, meta description before output ********/
66
+ //add_filter('wp_title', array('WpMetaSeo', 'new_title'), 99);
67
+ add_action('init', 'buffer_start');
68
+ add_action('wp_head', 'buffer_end');
69
+
70
+ function buffer_start() { ob_start("callback"); }
71
+
72
+ function buffer_end() { ob_end_flush(); }
73
+
74
+ function callback($buffer) {
75
+ // modify buffer here, and then return the updated code
76
+ global $wp_query;
77
+ $meta_title = get_post_meta($wp_query->post->ID, '_metaseo_metatitle', true);
78
+ $meta_description = get_post_meta($wp_query->post->ID, '_metaseo_metadesc', true);
79
+ $patterns = array(
80
+ '_title' => array('#<title>[^<>]+?<\/title>#i', '<title>'.$meta_title.'</title>',
81
+ ($meta_title != '' ? true : false) ),
82
+ 'title' => array(
83
+ '#<meta name="title" [^<>]+ ?>#i',
84
+ '<meta name="title" content="'. $meta_title .'" />',
85
+ ($meta_title != '' ? true : false) ),
86
+ 'description' => array(
87
+ '#<meta name="description" [^<>]+ ?>#i',
88
+ '<meta name="description" content="'. $meta_description .'" />',
89
+ ($meta_description != '' ? true : false) ),
90
+ 'og:title' => array(
91
+ '#<meta property="og:title" [^<>]+ ?>#i',
92
+ '<meta name="og:title" content="'. $meta_title .'" />',
93
+ ($meta_title != '' ? true : false) ),
94
+ 'og:description' => array(
95
+ '#<meta property="og:description" [^<>]+ ?>#i',
96
+ '<meta name="og:description" content="'. $meta_description .'" />',
97
+ ($meta_description != '' ? true : false) )
98
+ );
99
+
100
+ //
101
+ foreach($patterns as $k => $pattern){
102
+ if(preg_match_all($pattern[0], $buffer, $matches)){
103
+ $replacement = array();
104
+ foreach($matches[0] as $key => $match){
105
+ if($key < 1){
106
+ $replacement[] = $pattern[2] ? $pattern[1] : $match."\n";
107
+ } else { $replacement[] = ''; }
108
+ }
109
+
110
+ $buffer = str_ireplace($matches[0], $replacement, $buffer);
111
+ }
112
+ else{
113
+ $buffer = str_ireplace('</title>', "</title>\n" . $pattern[1], $buffer);
114
+ }
115
+ }
116
+
117
+ return $buffer;
118
+ }
119
+ /***********************************************/
120
+ }
121
+
122
+ /******** Check and import meta data from other installed plugins for SEO ********/
123
+ /**
124
+ * Handle import of meta data from other installed plugins for SEO
125
+ *
126
+ * @since 1.5.0
127
+ */
128
+ function wpmetaseo_aio_yoast_message() {
129
+ //update_option('_aio_import_notice_flag', 0);
130
+ //update_option('_yoast_import_notice_flag', 0);
131
+ $activated = 0;
132
+ // Check if All In One Pack is active
133
+ if(!get_option('_aio_import_notice_flag')){
134
+ if ( is_plugin_active( 'all-in-one-seo-pack/all_in_one_seo_pack.php' ) ) {
135
+ add_action( 'admin_notices', 'wpmetaseo_import_aio_meta_notice', 2 );
136
+ $activated++;
137
+ }
138
+
139
+ if(get_option('_aio_import_notice_flag') === false){
140
+ update_option('_aio_import_notice_flag', 0);
141
+ }
142
+ }
143
+ // Check if Yoast is active
144
+ if(!get_option('_yoast_import_notice_flag', false)){
145
+ if ( is_plugin_active( 'wordpress-seo/wp-seo.php' ) ) {
146
+ add_action( 'admin_notices', 'wpmetaseo_import_yoast_meta_notice', 3 );
147
+ $activated++;
148
+ }
149
+
150
+ if(get_option('_yoast_import_notice_flag') === false){
151
+ update_option('_yoast_import_notice_flag', 0);
152
+ }
153
+ }
154
+
155
+
156
+ if($activated === 2 && !get_option('plugin_to_sync_with', false)){
157
+ add_action('admin_notices', create_function('$notImportant', 'echo "<div class=\"error metaseo-import-wrn\"><p>". __("Be careful you installed 2 extensions doing almost the same thing, please deactivate AIOSEO or Yoast in order to work more clearly!", "wp-meta-seo") ."</p></div>";'), 1);
158
+ }
159
+ }
160
+
161
+ add_action( 'admin_init', 'wpmetaseo_aio_yoast_message' );
162
+
163
+ function wpmetaseo_import_aio_meta_notice(){
164
+ echo '<div class="error metaseo-import-wrn"><p>'. sprintf( __('We have found that you’re using All In One Pack Plugin, WP Meta SEO can import the meta from this plugin, %s', 'wp-meta-seo'), '<a href="#" class="button mseo-import-action" style="position:relative" onclick="importMetaData(this, event)" id="_aio_"><span class="spinner-light"></span>Import now</a> or <a href="#" class="dissmiss-import">dismiss this</a>' ) .'</p></div>';
165
+ }
166
+
167
+ function wpmetaseo_import_yoast_meta_notice(){
168
+ echo '<div class="error metaseo-import-wrn"><p>'. sprintf( __('We have found that you’re using Yoast SEO Plugin, WP Meta SEO can import the meta from this plugin, %s', 'wp-meta-seo'), '<a href="#" class="button mseo-import-action" style="position:relative" onclick="importMetaData(this, event)" id="_yoast_">Import now<span class="spinner-light"></span></a> or <a href="#" class="dissmiss-import">dismiss this</a>' ) .'</p></div>';
169
+ }
170
+
171
+ /**
172
+ * Encode or decode all values in string format of an array
173
+ */
174
+ function metaseo_utf8($obj, $action = 'encode'){
175
+ $action = strtolower(trim($action));
176
+ $fn = "utf8_$action";
177
+ if(is_array($obj)){
178
+ foreach($obj as &$el){
179
+ if(is_array($el)){
180
+ if(is_callable($fn)){
181
+ $el = metaseo_utf8($el, $action);
182
+ }
183
+ }
184
+ elseif(is_string($el)){
185
+ //var_dump(mb_detect_encoding($el));
186
+ $isASCII = mb_detect_encoding($el, 'ASCII');
187
+ if($action === 'encode' && !$isASCII){
188
+ $el = mb_convert_encoding($el, "UTF-8", "auto");
189
+ }
190
+
191
+ $el = $fn($el);
192
+ }
193
+ }
194
+ }elseif (is_object($obj)) {
195
+ $vars = array_keys(get_object_vars($obj));
196
+ foreach ($vars as $var) {
197
+ metaseo_utf8($obj->$var, $action);
198
+ }
199
+ }
200
+
201
+ return $obj;
202
+ }
203
+ /**********************************************************************************/