|
1
|
|
|
/** |
|
2
|
|
|
* Package: mivhak-js |
|
3
|
|
|
* URL: http://products.askupasoftware.com/mivhak-js |
|
4
|
|
|
* Version: 1.0.0 |
|
5
|
|
|
* Date: 2016-07-07 |
|
6
|
|
|
* Dependencies: jQuery Mousewheel, Ace Editor |
|
7
|
|
|
* License: GNU GENERAL PUBLIC LICENSE |
|
8
|
|
|
* |
|
9
|
|
|
* Developed by Askupa Software http://www.askupasoftware.com |
|
10
|
|
|
*/ |
|
11
|
|
|
/* test-code */ |
|
12
|
|
View Code Duplication |
var testapi = {}; |
|
|
|
|
|
|
13
|
|
|
/* end-test-code */ |
|
14
|
|
|
|
|
15
|
|
|
(function ( $ ) {// Ace global config |
|
16
|
|
|
ace.config.set('basePath', 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.3/');/** |
|
|
|
|
|
|
17
|
|
|
* Converts a string to it's actual value, if applicable |
|
18
|
|
|
* |
|
19
|
|
|
* @param {String} str |
|
20
|
|
|
*/ |
|
21
|
|
|
function strToValue( str ) |
|
22
|
|
|
{ |
|
23
|
|
|
if('true' === str.toLowerCase()) return true; |
|
|
|
|
|
|
24
|
|
|
if('false' === str.toLowerCase()) return false; |
|
|
|
|
|
|
25
|
|
|
if(!isNaN(str)) return parseFloat(str); |
|
|
|
|
|
|
26
|
|
|
return str; |
|
27
|
|
|
} |
|
28
|
|
|
|
|
29
|
|
|
/** |
|
30
|
|
|
* Convert hyphened text to camelCase. |
|
31
|
|
|
* |
|
32
|
|
|
* @param {string} str |
|
33
|
|
|
* @returns {string} |
|
34
|
|
|
*/ |
|
35
|
|
|
function toCamelCase( str ) |
|
36
|
|
|
{ |
|
37
|
|
|
return str.replace(/-(.)/g,function(match){ |
|
38
|
|
|
return match[1].toUpperCase(); |
|
39
|
|
|
}); |
|
40
|
|
|
} |
|
41
|
|
|
|
|
42
|
|
|
/** |
|
43
|
|
|
* Reads the element's 'miv-' attributes and returns their values as an object |
|
44
|
|
|
* |
|
45
|
|
|
* @param {DOMElement} el |
|
46
|
|
|
* @returns {Object} |
|
47
|
|
|
*/ |
|
48
|
|
|
function readAttributes( el ) |
|
49
|
|
|
{ |
|
50
|
|
|
var options = {}; |
|
51
|
|
|
$.each(el.attributes, function(i, attr){ |
|
52
|
|
|
if(/^miv-/.test(attr.name)) |
|
53
|
|
|
{ |
|
54
|
|
|
options[toCamelCase(attr.name.substr(4))] = strToValue(attr.value); |
|
55
|
|
|
} |
|
56
|
|
|
}); |
|
57
|
|
|
return options; |
|
58
|
|
|
} |
|
59
|
|
|
|
|
60
|
|
|
/** |
|
61
|
|
|
* Get the average value of all elements in the given array. |
|
62
|
|
|
* |
|
63
|
|
|
* @param {Array} arr |
|
64
|
|
|
* @returns {Number} |
|
65
|
|
|
*/ |
|
66
|
|
|
function average( arr ) |
|
67
|
|
|
{ |
|
68
|
|
|
var i = arr.length, sum = 0; |
|
69
|
|
|
while(i--) sum += parseFloat(arr[i]); |
|
|
|
|
|
|
70
|
|
|
return sum/arr.length; |
|
71
|
|
|
} |
|
72
|
|
|
|
|
73
|
|
|
/** |
|
74
|
|
|
* Get the maximum value of all elements in the given array. |
|
75
|
|
|
* |
|
76
|
|
|
* @param {Array} arr |
|
77
|
|
|
* @returns {Number} |
|
78
|
|
|
*/ |
|
79
|
|
|
function max( arr ) |
|
80
|
|
|
{ |
|
81
|
|
|
var i = arr.length, maxval = arr[--i]; |
|
82
|
|
|
while(i--) if(arr[i] > maxval) maxval = arr[i]; |
|
|
|
|
|
|
83
|
|
|
return maxval; |
|
84
|
|
|
} |
|
85
|
|
|
|
|
86
|
|
|
/** |
|
87
|
|
|
* Get the minimum value of all elements in the given array. |
|
88
|
|
|
* |
|
89
|
|
|
* @param {Array} arr |
|
90
|
|
|
* @returns {Number} |
|
91
|
|
|
*/ |
|
92
|
|
|
function min( arr ) |
|
93
|
|
|
{ |
|
94
|
|
|
var i = arr.length, minval = arr[--i]; |
|
95
|
|
|
while(i--) if(arr[i] < minval) minval = arr[i]; |
|
|
|
|
|
|
96
|
|
|
return minval; |
|
97
|
|
|
} |
|
98
|
|
|
|
|
99
|
|
|
/** |
|
100
|
|
|
* Calculate the editor's height based on the number of lines & line height. |
|
101
|
|
|
* |
|
102
|
|
|
* @param {jQuery} $editor Ther editor wrapper element (PRE) |
|
103
|
|
|
* @returns {Number} |
|
104
|
|
|
*/ |
|
105
|
|
|
function getEditorHeight( $editor ) |
|
106
|
|
|
{ |
|
107
|
|
|
var height = 0; |
|
108
|
|
|
$editor.find('.ace_text-layer').children().each(function(){ |
|
109
|
|
|
height += $(this).height(); |
|
110
|
|
|
}); |
|
111
|
|
|
return height; |
|
112
|
|
|
} |
|
113
|
|
|
|
|
114
|
|
|
/** |
|
115
|
|
|
* Convert a string like "3, 5-7" into an array of ranges in to form of |
|
116
|
|
|
* [ |
|
117
|
|
|
* {start:2, end:2}, |
|
118
|
|
|
* {start:4, end:6}, |
|
119
|
|
|
* ] |
|
120
|
|
|
* The string should be given as a list if comma delimited 1 based ranges. |
|
121
|
|
|
* The result is given as a 0 based array of ranges. |
|
122
|
|
|
* |
|
123
|
|
|
* @param {string} str |
|
124
|
|
|
* @returns {Array} |
|
125
|
|
|
*/ |
|
126
|
|
|
function strToRange( str ) |
|
127
|
|
|
{ |
|
128
|
|
|
var range = str.replace(' ', '').split(','), |
|
129
|
|
|
i = range.length, |
|
130
|
|
|
ranges = [], |
|
131
|
|
|
start, end, splitted; |
|
132
|
|
|
|
|
133
|
|
|
while(i--) |
|
134
|
|
|
{ |
|
135
|
|
|
// Multiple lines highlight |
|
136
|
|
|
if( range[i].indexOf('-') > -1 ) |
|
137
|
|
|
{ |
|
138
|
|
|
splitted = range[i].split('-'); |
|
139
|
|
|
start = parseInt(splitted[0])-1; |
|
140
|
|
|
end = parseInt(splitted[1])-1; |
|
141
|
|
|
} |
|
142
|
|
|
|
|
143
|
|
|
// Single line highlight |
|
144
|
|
|
else |
|
145
|
|
|
{ |
|
146
|
|
|
start = parseInt(range[i])-1; |
|
147
|
|
|
end = start; |
|
148
|
|
|
} |
|
149
|
|
|
|
|
150
|
|
|
ranges.unshift({start:start,end:end}); |
|
151
|
|
|
} |
|
152
|
|
|
|
|
153
|
|
|
return ranges; |
|
154
|
|
|
} |
|
155
|
|
|
|
|
156
|
|
|
/** |
|
157
|
|
|
* Request animation frame. Uses setTimeout as a fallback if the browser does |
|
158
|
|
|
* not support requestAnimationFrame (based on 60 frames per second). |
|
159
|
|
|
* |
|
160
|
|
|
* @param {type} cb |
|
161
|
|
|
* @returns {Number} |
|
162
|
|
|
*/ |
|
163
|
|
|
var raf = window.requestAnimationFrame || |
|
164
|
|
|
window.webkitRequestAnimationFrame || |
|
165
|
|
|
window.mozRequestAnimationFrame || |
|
166
|
|
|
window.msRequestAnimationFrame || |
|
167
|
|
|
function(cb) { return window.setTimeout(cb, 1000 / 60); }; |
|
168
|
|
|
|
|
169
|
|
|
/* test-code */ |
|
170
|
|
|
testapi.strToValue = strToValue; |
|
171
|
|
|
testapi.toCamelCase = toCamelCase; |
|
172
|
|
|
testapi.readAttributes = readAttributes; |
|
173
|
|
|
testapi.average = average; |
|
174
|
|
|
testapi.max = max; |
|
175
|
|
|
testapi.min = min; |
|
176
|
|
|
testapi.getEditorHeight = getEditorHeight; |
|
177
|
|
|
testapi.strToRange = strToRange; |
|
178
|
|
|
testapi.raf = raf; |
|
179
|
|
|
/* end-test-code *//** |
|
180
|
|
|
* The constructor. |
|
181
|
|
|
* See Mivhal.defaults for available options. |
|
182
|
|
|
* |
|
183
|
|
|
* @param {DOMElement} selection |
|
184
|
|
|
* @param {Object} options |
|
185
|
|
|
*/ |
|
186
|
|
|
function Mivhak( selection, options ) |
|
187
|
|
|
{ |
|
188
|
|
|
// Bail if there are no resources |
|
189
|
|
|
if(!selection.getElementsByTagName('PRE').length) return; |
|
|
|
|
|
|
190
|
|
|
|
|
191
|
|
|
this.$selection = $( selection ); |
|
192
|
|
|
this.setOptions( options ); |
|
193
|
|
|
this.init(); |
|
194
|
|
|
} |
|
195
|
|
|
|
|
196
|
|
|
/** |
|
197
|
|
|
* Check if a given string represents a supported method |
|
198
|
|
|
* @param {string} method |
|
199
|
|
|
*/ |
|
200
|
|
|
Mivhak.methodExists = function( method ) |
|
201
|
|
|
{ |
|
202
|
|
|
return typeof method === 'string' && Mivhak.methods[method]; |
|
203
|
|
|
}; |
|
204
|
|
|
|
|
205
|
|
|
/** |
|
206
|
|
|
* Initiate the code viewer. |
|
207
|
|
|
*/ |
|
208
|
|
|
Mivhak.prototype.init = function() |
|
209
|
|
|
{ |
|
210
|
|
|
this.initState(); |
|
211
|
|
|
this.parseResources(); |
|
212
|
|
|
this.createUI(); |
|
213
|
|
|
this.applyOptions(); |
|
214
|
|
|
this.callMethod('showTab',0); // Show first tab initially |
|
215
|
|
|
}; |
|
216
|
|
|
|
|
217
|
|
|
/** |
|
218
|
|
|
* Apply the options that were set by the user. This function is called when |
|
219
|
|
|
* Mivhak is initiated, and every time the options are updated. |
|
220
|
|
|
*/ |
|
221
|
|
|
Mivhak.prototype.applyOptions = function() |
|
222
|
|
|
{ |
|
223
|
|
|
this.callMethod('setHeight', this.options.height); |
|
224
|
|
|
this.callMethod('setAccentColor', this.options.accentColor); |
|
225
|
|
|
if(this.options.collapsed) this.callMethod('collapse'); |
|
|
|
|
|
|
226
|
|
|
if(!this.options.topbar) this.$selection.addClass('mivhak-no-topbar'); |
|
|
|
|
|
|
227
|
|
|
else this.$selection.removeClass('mivhak-no-topbar'); |
|
228
|
|
|
|
|
229
|
|
|
this.createCaption(); |
|
230
|
|
|
this.createLivePreview(); |
|
231
|
|
|
}; |
|
232
|
|
|
|
|
233
|
|
|
/** |
|
234
|
|
|
* Initiate this instance's state. |
|
235
|
|
|
*/ |
|
236
|
|
|
Mivhak.prototype.initState = function() |
|
237
|
|
|
{ |
|
238
|
|
|
this.state = { |
|
239
|
|
|
lineWrap: true, |
|
240
|
|
|
collapsed: false, |
|
241
|
|
|
height: 0, |
|
242
|
|
|
activeTab: null, // Updated by tabs.showTab |
|
243
|
|
|
resources: [] // Generated by parseResources() |
|
244
|
|
|
}; |
|
245
|
|
|
}; |
|
246
|
|
|
|
|
247
|
|
|
/** |
|
248
|
|
|
* Set or update this instance's options. |
|
249
|
|
|
* @param {object} options |
|
250
|
|
|
*/ |
|
251
|
|
|
Mivhak.prototype.setOptions = function( options ) |
|
252
|
|
|
{ |
|
253
|
|
|
// If options were already set, update them |
|
254
|
|
|
if( typeof this.options !== 'undefined' ) |
|
255
|
|
|
this.options = $.extend(true, {}, this.options, options, readAttributes(this.$selection[0])); |
|
|
|
|
|
|
256
|
|
|
|
|
257
|
|
|
// Otherwise, merge them with the defaults |
|
258
|
|
|
else this.options = $.extend(true, {}, Mivhak.defaults, options, readAttributes(this.$selection[0])); |
|
259
|
|
|
}; |
|
260
|
|
|
|
|
261
|
|
|
/** |
|
262
|
|
|
* Call one of Mivhak's methods. See Mivhak.methods for available methods. |
|
263
|
|
|
* To apply additional arguments, simply pass the arguments after the methodName |
|
264
|
|
|
* i.e. callMethod('methodName', arg1, arg2). |
|
265
|
|
|
* This method is also called internally when making a method call through jQuery |
|
266
|
|
|
* i.e. $('#el').mivhak('methodName', arg1, arg2); |
|
267
|
|
|
* |
|
268
|
|
|
* @param {string} methodName |
|
269
|
|
|
*/ |
|
270
|
|
|
Mivhak.prototype.callMethod = function( methodName ) |
|
271
|
|
|
{ |
|
272
|
|
|
if(Mivhak.methodExists(methodName)) |
|
273
|
|
|
{ |
|
274
|
|
|
// Call the method with the original arguments, removing the method's name from the list |
|
275
|
|
|
var args = []; |
|
276
|
|
|
Array.prototype.push.apply( args, arguments ); |
|
277
|
|
|
args.shift(); |
|
278
|
|
|
Mivhak.methods[methodName].apply(this, args); |
|
279
|
|
|
} |
|
280
|
|
|
}; |
|
281
|
|
|
|
|
282
|
|
|
/** |
|
283
|
|
|
* Create the user interface. |
|
284
|
|
|
*/ |
|
285
|
|
|
Mivhak.prototype.createUI = function() |
|
286
|
|
|
{ |
|
287
|
|
|
this.tabs = Mivhak.render('tabs',{mivhakInstance: this}); |
|
288
|
|
|
this.topbar = Mivhak.render('top-bar',{mivhakInstance: this}); |
|
289
|
|
|
this.notifier = Mivhak.render('notifier'); |
|
290
|
|
|
|
|
291
|
|
|
this.$selection.prepend(this.tabs.$el); |
|
292
|
|
|
this.$selection.prepend(this.topbar.$el); |
|
293
|
|
|
this.tabs.$el.prepend(this.notifier.$el); |
|
294
|
|
|
}; |
|
295
|
|
|
|
|
296
|
|
|
/** |
|
297
|
|
|
* Calculate the height in pixels. |
|
298
|
|
|
* |
|
299
|
|
|
* auto: Automatically calculate the height based on the number of lines. |
|
300
|
|
|
* min: Calculate the height based on the height of the tab with the maximum number of lines |
|
301
|
|
|
* max: Calculate the height based on the height of the tab with the minimum number of lines |
|
302
|
|
|
* average: Calculate the height based on the average height of all tabs |
|
303
|
|
|
* |
|
304
|
|
|
* @param {string|number} h One of (auto|min|max|average) or a custom number |
|
305
|
|
|
* @returns {Number} |
|
306
|
|
|
*/ |
|
307
|
|
|
Mivhak.prototype.calculateHeight = function(h) |
|
308
|
|
|
{ |
|
309
|
|
|
var heights = [], |
|
310
|
|
|
padding = this.options.padding*2, |
|
311
|
|
|
i = this.tabs.tabs.length; |
|
312
|
|
|
|
|
313
|
|
|
while(i--) |
|
314
|
|
|
heights.push(getEditorHeight($(this.tabs.tabs[i].resource.pre))+padding); |
|
|
|
|
|
|
315
|
|
|
|
|
316
|
|
|
if('average' === h) return average(heights); |
|
|
|
|
|
|
317
|
|
|
if('shortest' === h) return min(heights); |
|
|
|
|
|
|
318
|
|
|
if('longest' === h) return max(heights); |
|
|
|
|
|
|
319
|
|
|
if('auto' === h) return getEditorHeight($(this.activeTab.resource.pre))+padding; |
|
|
|
|
|
|
320
|
|
|
if(!isNaN(h)) return parseInt(h); |
|
|
|
|
|
|
321
|
|
|
}; |
|
322
|
|
|
|
|
323
|
|
|
/** |
|
324
|
|
|
* Loop through each PRE element inside this.$selection and store it's options |
|
325
|
|
|
* in this.resources, merging it with the default option values. |
|
326
|
|
|
*/ |
|
327
|
|
|
Mivhak.prototype.parseResources = function() |
|
328
|
|
|
{ |
|
329
|
|
|
var $this = this; |
|
330
|
|
|
|
|
331
|
|
|
this.resources = new Resources(); |
|
332
|
|
|
this.$selection.find('pre').each(function(){ |
|
333
|
|
|
$this.resources.add(this); |
|
334
|
|
|
}); |
|
335
|
|
|
}; |
|
336
|
|
|
|
|
337
|
|
|
Mivhak.prototype.createCaption = function() |
|
338
|
|
|
{ |
|
339
|
|
|
if(this.options.caption) |
|
340
|
|
|
{ |
|
341
|
|
|
if(!this.caption) |
|
342
|
|
|
{ |
|
343
|
|
|
this.caption = Mivhak.render('caption',{text: this.options.caption}); |
|
344
|
|
|
this.$selection.append(this.caption.$el); |
|
345
|
|
|
} |
|
346
|
|
|
else this.caption.setText(this.options.caption); |
|
|
|
|
|
|
347
|
|
|
} |
|
348
|
|
|
else this.$selection.addClass('mivhak-no-caption'); |
|
|
|
|
|
|
349
|
|
|
}; |
|
350
|
|
|
|
|
351
|
|
|
/** |
|
352
|
|
|
* Create the live preview iframe window |
|
353
|
|
|
*/ |
|
354
|
|
|
Mivhak.prototype.createLivePreview = function() |
|
355
|
|
|
{ |
|
356
|
|
|
if(this.options.runnable && typeof this.preview === 'undefined') |
|
357
|
|
|
{ |
|
358
|
|
|
this.preview = Mivhak.render('live-preview',{resources: this.resources}); |
|
359
|
|
|
this.tabs.$el.append(this.preview.$el); |
|
360
|
|
|
} |
|
361
|
|
|
}; |
|
362
|
|
|
|
|
363
|
|
|
/** |
|
364
|
|
|
* Remove all generated elements, data and events. |
|
365
|
|
|
* |
|
366
|
|
|
* TODO: keep initial HTML |
|
367
|
|
|
*/ |
|
368
|
|
|
Mivhak.prototype.destroy = function() |
|
369
|
|
|
{ |
|
370
|
|
|
this.$selection.empty(); |
|
371
|
|
|
}; |
|
372
|
|
|
|
|
373
|
|
|
/* test-code */ |
|
374
|
|
|
testapi.mivhak = Mivhak; |
|
375
|
|
|
/* end-test-code *//** |
|
376
|
|
|
* A list of Mivhak default options |
|
377
|
|
|
*/ |
|
378
|
|
|
Mivhak.defaults = { |
|
379
|
|
|
|
|
380
|
|
|
/** |
|
381
|
|
|
* Whether to add a live preview (and a "play" button) to run the code |
|
382
|
|
|
* @type Boolean |
|
383
|
|
|
*/ |
|
384
|
|
|
runnable: false, |
|
385
|
|
|
|
|
386
|
|
|
/** |
|
387
|
|
|
* Whther to allow the user to edit the code |
|
388
|
|
|
* @type Boolean |
|
389
|
|
|
*/ |
|
390
|
|
|
editable: false, |
|
391
|
|
|
|
|
392
|
|
|
/** |
|
393
|
|
|
* Whether to show line numers on the left |
|
394
|
|
|
* @type Boolean |
|
395
|
|
|
*/ |
|
396
|
|
|
lineNumbers: false, |
|
397
|
|
|
|
|
398
|
|
|
/** |
|
399
|
|
|
* One of the supported CSS color values (HEX, RGB, etc...) to set as the |
|
400
|
|
|
* code viewer's accent color. Controls the scrollbars, tab navigation and |
|
401
|
|
|
* dropdown item colors. |
|
402
|
|
|
* @type String |
|
403
|
|
|
*/ |
|
404
|
|
|
accentColor: false, |
|
405
|
|
|
|
|
406
|
|
|
/** |
|
407
|
|
|
* Whether to collapse the code viewer initially |
|
408
|
|
|
* @type Boolean |
|
409
|
|
|
*/ |
|
410
|
|
|
collapsed: false, |
|
411
|
|
|
|
|
412
|
|
|
/** |
|
413
|
|
|
* Text/HTML string to be displayed at the bottom of the code viewer |
|
414
|
|
|
* @type Boolean|string |
|
415
|
|
|
*/ |
|
416
|
|
|
caption: false, |
|
417
|
|
|
|
|
418
|
|
|
/** |
|
419
|
|
|
* The code viewer's theme. One of (dark|light) |
|
420
|
|
|
* @type String |
|
421
|
|
|
*/ |
|
422
|
|
|
theme: 'light', |
|
423
|
|
|
|
|
424
|
|
|
/** |
|
425
|
|
|
* The code viewer's height. Either a number (for a custom height in pixels) |
|
426
|
|
|
* or one of (auto|min|max|average). |
|
427
|
|
|
* @type String|Number |
|
428
|
|
|
*/ |
|
429
|
|
|
height: 'average', |
|
430
|
|
|
|
|
431
|
|
|
/** |
|
432
|
|
|
* The surrounding padding between the code and the wrapper. |
|
433
|
|
|
* @type Number |
|
434
|
|
|
*/ |
|
435
|
|
|
padding: 15, |
|
436
|
|
|
|
|
437
|
|
|
/** |
|
438
|
|
|
* Whether to show/hide the top bar |
|
439
|
|
|
* @type Boolean |
|
440
|
|
|
*/ |
|
441
|
|
|
topbar: true, |
|
442
|
|
|
|
|
443
|
|
|
/** |
|
444
|
|
|
* An array of strings/objects for the settings dropdown menu |
|
445
|
|
|
* @type Array |
|
446
|
|
|
*/ |
|
447
|
|
|
buttons: ['wrap','copy','collapse','about'] |
|
448
|
|
|
}; |
|
449
|
|
|
|
|
450
|
|
|
/** |
|
451
|
|
|
* A list of Mivhak resource default settings (Mivhak resources are any <pre> |
|
452
|
|
|
* elements placed inside a Mivhak wrapper element). |
|
453
|
|
|
*/ |
|
454
|
|
|
Mivhak.resourceDefaults = { |
|
455
|
|
|
|
|
456
|
|
|
/** |
|
457
|
|
|
* The resource language (one of the supported Ace Editor languages) |
|
458
|
|
|
* @type string |
|
459
|
|
|
*/ |
|
460
|
|
|
lang: null, |
|
461
|
|
|
|
|
462
|
|
|
/** |
|
463
|
|
|
* How the resource should be treated in the preview window. One of (script|style|markup) |
|
464
|
|
|
* @type bool|string |
|
465
|
|
|
*/ |
|
466
|
|
|
runAs: false, |
|
467
|
|
|
|
|
468
|
|
|
/** |
|
469
|
|
|
* A URL to an external source |
|
470
|
|
|
* @type bool|string |
|
471
|
|
|
*/ |
|
472
|
|
|
source: false, |
|
473
|
|
|
|
|
474
|
|
|
/** |
|
475
|
|
|
* Whether to show this resource as a tab. Useful if you want to include |
|
476
|
|
|
* external libraries for the live preview and don't need to see their contents. |
|
477
|
|
|
* @type Boolean |
|
478
|
|
|
*/ |
|
479
|
|
|
visible: true, |
|
480
|
|
|
|
|
481
|
|
|
/** |
|
482
|
|
|
* Mark/highlight a range of lines given as a string in the format '1, 3-4' |
|
483
|
|
|
* @type bool|string |
|
484
|
|
|
*/ |
|
485
|
|
|
mark: false, |
|
486
|
|
|
|
|
487
|
|
|
/** |
|
488
|
|
|
* Set the initial line number (1 based). |
|
489
|
|
|
* @type Number |
|
490
|
|
|
*/ |
|
491
|
|
|
startLine: 1 |
|
492
|
|
|
};var Resources = function() { |
|
493
|
|
|
this.data = []; |
|
494
|
|
|
}; |
|
495
|
|
|
|
|
496
|
|
|
Resources.prototype.count = function() { |
|
497
|
|
|
return this.data.length; |
|
498
|
|
|
}; |
|
499
|
|
|
|
|
500
|
|
|
Resources.prototype.add = function(pre) { |
|
501
|
|
|
this.data.push($.extend({}, |
|
502
|
|
|
Mivhak.resourceDefaults,{ |
|
503
|
|
|
pre:pre, |
|
504
|
|
|
content: pre.textContent |
|
505
|
|
|
}, |
|
506
|
|
|
readAttributes(pre) |
|
507
|
|
|
)); |
|
508
|
|
|
}; |
|
509
|
|
|
|
|
510
|
|
|
Resources.prototype.get = function(i) { |
|
511
|
|
|
return this.data[i]; |
|
512
|
|
|
}; |
|
513
|
|
|
|
|
514
|
|
|
Resources.prototype.update = function(i, content) { |
|
515
|
|
|
this.data[i].content = content; |
|
516
|
|
|
}; |
|
517
|
|
|
|
|
518
|
|
|
// Built-in buttons |
|
519
|
|
|
Mivhak.buttons = { |
|
520
|
|
|
|
|
521
|
|
|
/** |
|
522
|
|
|
* The wrap button features a toggle button and is used to toggle line wrap |
|
523
|
|
|
* on/off for the currently active tab |
|
524
|
|
|
*/ |
|
525
|
|
|
wrap: { |
|
526
|
|
|
text: 'Wrap Lines', |
|
527
|
|
|
toggle: true, |
|
528
|
|
|
click: function(e) { |
|
529
|
|
|
e.stopPropagation(); |
|
530
|
|
|
this.callMethod('toggleLineWrap'); |
|
531
|
|
|
} |
|
532
|
|
|
}, |
|
533
|
|
|
|
|
534
|
|
|
/** |
|
535
|
|
|
* The copy button copies the code in the currently active tab to clipboard |
|
536
|
|
|
* (except for Safari, where it selects the code and prompts the user to press command+c) |
|
537
|
|
|
*/ |
|
538
|
|
|
copy: { |
|
539
|
|
|
text: 'Copy', |
|
540
|
|
|
click: function(e) { |
|
|
|
|
|
|
541
|
|
|
this.callMethod('copyCode'); |
|
542
|
|
|
} |
|
543
|
|
|
}, |
|
544
|
|
|
|
|
545
|
|
|
/** |
|
546
|
|
|
* The collapse button toggles the entire code viewer into and out of its |
|
547
|
|
|
* collapsed state. |
|
548
|
|
|
*/ |
|
549
|
|
|
collapse: { |
|
550
|
|
|
text: 'Colllapse', |
|
551
|
|
|
click: function(e) { |
|
|
|
|
|
|
552
|
|
|
this.callMethod('collapse'); |
|
553
|
|
|
} |
|
554
|
|
|
}, |
|
555
|
|
|
|
|
556
|
|
|
/** |
|
557
|
|
|
* The about button shows the user information about Mivhak |
|
558
|
|
|
*/ |
|
559
|
|
|
about: { |
|
560
|
|
|
text: 'About Mivhak', |
|
561
|
|
|
click: function(e) { |
|
|
|
|
|
|
562
|
|
|
this.notifier.closableNotification('Mivhak.js v1.0.0'); |
|
563
|
|
|
} |
|
564
|
|
|
} |
|
565
|
|
|
};/** |
|
566
|
|
|
* jQuery plugin's methods. |
|
567
|
|
|
* In all methods, the 'this' keyword is pointing to the calling instance of Mivhak. |
|
568
|
|
|
* These functions serve as the plugin's public API. |
|
569
|
|
|
*/ |
|
570
|
|
|
Mivhak.methods = { |
|
571
|
|
|
|
|
572
|
|
|
/** |
|
573
|
|
|
* Toggle line wrap on/off for the currently active tab. Initially set to |
|
574
|
|
|
* on (true) by default. |
|
575
|
|
|
*/ |
|
576
|
|
|
toggleLineWrap: function() { |
|
577
|
|
|
var $this = this; |
|
578
|
|
|
this.state.lineWrap = !this.state.lineWrap; |
|
579
|
|
|
$.each(this.tabs.tabs, function(i,tab) { |
|
580
|
|
|
tab.editor.getSession().setUseWrapMode($this.state.lineWrap); |
|
581
|
|
|
tab.vscroll.refresh(); |
|
582
|
|
|
tab.hscroll.refresh(); |
|
583
|
|
|
}); |
|
584
|
|
|
}, |
|
585
|
|
|
|
|
586
|
|
|
/** |
|
587
|
|
|
* copy the code in the currently active tab to clipboard (works in all |
|
588
|
|
|
* browsers apart from Safari, where it selects the code and prompts the |
|
589
|
|
|
* user to press command+c) |
|
590
|
|
|
*/ |
|
591
|
|
|
copyCode: function() { |
|
592
|
|
|
var editor = this.activeTab.editor; |
|
593
|
|
|
editor.selection.selectAll(); |
|
594
|
|
|
editor.focus(); |
|
595
|
|
|
if(document.execCommand('copy')) { |
|
596
|
|
|
editor.selection.clearSelection(); |
|
597
|
|
|
this.notifier.timedNotification('Copied to clipboard!', 2000); |
|
598
|
|
|
} |
|
599
|
|
|
else this.notifier.timedNotification('Press ⌘+C to copy the code', 2000); |
|
|
|
|
|
|
600
|
|
|
}, |
|
601
|
|
|
|
|
602
|
|
|
/** |
|
603
|
|
|
* Collapse the code viewer and show a "Show Code" button. |
|
604
|
|
|
*/ |
|
605
|
|
|
collapse: function() { |
|
606
|
|
|
if(this.state.collapsed) return; |
|
|
|
|
|
|
607
|
|
|
var $this = this; |
|
608
|
|
|
this.state.collapsed = true; |
|
609
|
|
|
this.notifier.closableNotification('Show Code', function(){$this.callMethod('expand');}); |
|
610
|
|
|
this.$selection.addClass('mivhak-collapsed'); |
|
611
|
|
|
this.callMethod('setHeight',this.notifier.$el.outerHeight(true)); |
|
612
|
|
|
}, |
|
613
|
|
|
|
|
614
|
|
|
/** |
|
615
|
|
|
* Expand the code viewer if it's collapsed; |
|
616
|
|
|
*/ |
|
617
|
|
|
expand: function() { |
|
618
|
|
|
if(!this.state.collapsed) return; |
|
|
|
|
|
|
619
|
|
|
this.state.collapsed = false; |
|
620
|
|
|
this.notifier.hide(); // In case it's called by an external script |
|
621
|
|
|
this.$selection.removeClass('mivhak-collapsed'); |
|
622
|
|
|
this.callMethod('setHeight',this.options.height); |
|
623
|
|
|
}, |
|
624
|
|
|
|
|
625
|
|
|
/** |
|
626
|
|
|
* Show/activate a tab by the given index (zero based). |
|
627
|
|
|
* @param {number} index |
|
628
|
|
|
*/ |
|
629
|
|
|
showTab: function(index) { |
|
630
|
|
|
this.tabs.showTab(index); |
|
631
|
|
|
this.topbar.activateNavTab(index); |
|
632
|
|
|
if(this.options.runnable) |
|
633
|
|
|
this.preview.hide(); |
|
|
|
|
|
|
634
|
|
|
}, |
|
635
|
|
|
|
|
636
|
|
|
/** |
|
637
|
|
|
* Set the height of the code viewer. One of (auto|min|max|average) or |
|
638
|
|
|
* a custom number. |
|
639
|
|
|
* @param {string|number} height |
|
640
|
|
|
*/ |
|
641
|
|
|
setHeight: function(height) { |
|
642
|
|
|
var $this = this; |
|
643
|
|
|
raf(function(){ |
|
644
|
|
|
$this.state.height = $this.calculateHeight(height); |
|
645
|
|
|
$this.tabs.$el.height($this.state.height); |
|
646
|
|
|
$.each($this.tabs.tabs, function(i,tab) { |
|
647
|
|
|
$(tab.resource.pre).height(height); |
|
648
|
|
|
tab.editor.resize(); |
|
649
|
|
|
tab.vscroll.refresh(); |
|
650
|
|
|
tab.hscroll.refresh(); |
|
651
|
|
|
}); |
|
652
|
|
|
}); |
|
653
|
|
|
}, |
|
654
|
|
|
|
|
655
|
|
|
/** |
|
656
|
|
|
* Set the code viewer's accent color. Applied to the nav-tabs text color, |
|
657
|
|
|
* underline, scrollbars and dropdown menu text color. |
|
658
|
|
|
* |
|
659
|
|
|
* @param {string} color |
|
660
|
|
|
*/ |
|
661
|
|
|
setAccentColor: function(color) { |
|
662
|
|
|
if(!color) return; |
|
|
|
|
|
|
663
|
|
|
this.topbar.$el.find('.mivhak-top-bar-button').css({'color': color}); |
|
664
|
|
|
this.topbar.$el.find('.mivhak-dropdown-button').css({'color': color}); |
|
665
|
|
|
this.topbar.$el.find('.mivhak-controls svg').css({'fill': color}); |
|
666
|
|
|
this.tabs.$el.find('.mivhak-scrollbar-thumb').css({'background-color': color}); |
|
667
|
|
|
this.topbar.line.css({'background-color': color}); |
|
668
|
|
|
} |
|
669
|
|
|
};Mivhak.icons = {}; |
|
670
|
|
|
|
|
671
|
|
|
// <div>Icons made by <a href="http://www.flaticon.com/authors/egor-rumyantsev" title="Egor Rumyantsev">Egor Rumyantsev</a> from <a href="http://www.flaticon.com" title="Flaticon">www.flaticon.com</a> is licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a></div> |
|
672
|
|
|
Mivhak.icons.play = '<svg viewBox="0 0 232.153 232.153"><g><path style="fill-rule:evenodd;clip-rule:evenodd;" d="M203.791,99.628L49.307,2.294c-4.567-2.719-10.238-2.266-14.521-2.266c-17.132,0-17.056,13.227-17.056,16.578v198.94c0,2.833-0.075,16.579,17.056,16.579c4.283,0,9.955,0.451,14.521-2.267l154.483-97.333c12.68-7.545,10.489-16.449,10.489-16.449S216.471,107.172,203.791,99.628z"/></g></svg>'; |
|
673
|
|
|
|
|
674
|
|
|
// <div>Icons made by <a href="http://www.flaticon.com/authors/dave-gandy" title="Dave Gandy">Dave Gandy</a> from <a href="http://www.flaticon.com" title="Flaticon">www.flaticon.com</a> is licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a></div> |
|
675
|
|
|
Mivhak.icons.cog = '<svg viewbox="0 0 438.529 438.529"><g><path d="M436.25,181.438c-1.529-2.002-3.524-3.193-5.995-3.571l-52.249-7.992c-2.854-9.137-6.756-18.461-11.704-27.98c3.422-4.758,8.559-11.466,15.41-20.129c6.851-8.661,11.703-14.987,14.561-18.986c1.523-2.094,2.279-4.281,2.279-6.567c0-2.663-0.66-4.755-1.998-6.28c-6.848-9.708-22.552-25.885-47.106-48.536c-2.275-1.903-4.661-2.854-7.132-2.854c-2.857,0-5.14,0.855-6.854,2.567l-40.539,30.549c-7.806-3.999-16.371-7.52-25.693-10.565l-7.994-52.529c-0.191-2.474-1.287-4.521-3.285-6.139C255.95,0.806,253.623,0,250.954,0h-63.38c-5.52,0-8.947,2.663-10.278,7.993c-2.475,9.513-5.236,27.214-8.28,53.1c-8.947,2.86-17.607,6.476-25.981,10.853l-39.399-30.549c-2.474-1.903-4.948-2.854-7.422-2.854c-4.187,0-13.179,6.804-26.979,20.413c-13.8,13.612-23.169,23.841-28.122,30.69c-1.714,2.474-2.568,4.664-2.568,6.567c0,2.286,0.95,4.57,2.853,6.851c12.751,15.42,22.936,28.549,30.55,39.403c-4.759,8.754-8.47,17.511-11.132,26.265l-53.105,7.992c-2.093,0.382-3.9,1.621-5.424,3.715C0.76,182.531,0,184.722,0,187.002v63.383c0,2.478,0.76,4.709,2.284,6.708c1.524,1.998,3.521,3.195,5.996,3.572l52.25,7.71c2.663,9.325,6.564,18.743,11.704,28.257c-3.424,4.761-8.563,11.468-15.415,20.129c-6.851,8.665-11.709,14.989-14.561,18.986c-1.525,2.102-2.285,4.285-2.285,6.57c0,2.471,0.666,4.658,1.997,6.561c7.423,10.284,23.125,26.272,47.109,47.969c2.095,2.094,4.475,3.138,7.137,3.138c2.857,0,5.236-0.852,7.138-2.563l40.259-30.553c7.808,3.997,16.371,7.519,25.697,10.568l7.993,52.529c0.193,2.471,1.287,4.518,3.283,6.14c1.997,1.622,4.331,2.423,6.995,2.423h63.38c5.53,0,8.952-2.662,10.287-7.994c2.471-9.514,5.229-27.213,8.274-53.098c8.946-2.858,17.607-6.476,25.981-10.855l39.402,30.84c2.663,1.712,5.141,2.563,7.42,2.563c4.186,0,13.131-6.752,26.833-20.27c13.709-13.511,23.13-23.79,28.264-30.837c1.711-1.902,2.569-4.09,2.569-6.561c0-2.478-0.947-4.862-2.857-7.139c-13.698-16.754-23.883-29.882-30.546-39.402c3.806-7.043,7.519-15.701,11.136-25.98l52.817-7.988c2.279-0.383,4.189-1.622,5.708-3.716c1.523-2.098,2.279-4.288,2.279-6.571v-63.376C438.533,185.671,437.777,183.438,436.25,181.438z M270.946,270.939c-14.271,14.277-31.497,21.416-51.676,21.416c-20.177,0-37.401-7.139-51.678-21.416c-14.272-14.271-21.411-31.498-21.411-51.673c0-20.177,7.135-37.401,21.411-51.678c14.277-14.272,31.504-21.411,51.678-21.411c20.179,0,37.406,7.139,51.676,21.411c14.274,14.277,21.413,31.501,21.413,51.678C292.359,239.441,285.221,256.669,270.946,270.939z"/></g></svg>';/** |
|
676
|
|
|
* The list of registered components. |
|
677
|
|
|
* |
|
678
|
|
|
* @type Array |
|
679
|
|
|
*/ |
|
680
|
|
|
Mivhak.components = []; |
|
681
|
|
|
|
|
682
|
|
|
/** |
|
683
|
|
|
* Register a new component |
|
684
|
|
|
* |
|
685
|
|
|
* @param {string} name The components name |
|
686
|
|
|
* @param {Object} options A list of component properties |
|
687
|
|
|
*/ |
|
688
|
|
|
Mivhak.component = function(name, options) |
|
689
|
|
|
{ |
|
690
|
|
|
Mivhak.components[name] = options; |
|
691
|
|
|
}; |
|
692
|
|
|
|
|
693
|
|
|
/** |
|
694
|
|
|
* Render a new component |
|
695
|
|
|
* |
|
696
|
|
|
* TODO: move this into a seperate library |
|
697
|
|
|
* |
|
698
|
|
|
* @param {string} name The components name |
|
699
|
|
|
* @param {Object} props A list of component properties. |
|
700
|
|
|
* This overrides the component's initial property values. |
|
701
|
|
|
*/ |
|
702
|
|
|
Mivhak.render = function(name, props) |
|
703
|
|
|
{ |
|
704
|
|
|
var component = $.extend(true, {}, Mivhak.components[name]); |
|
705
|
|
|
var el = {}; |
|
706
|
|
|
|
|
707
|
|
|
// Create the element from the template |
|
708
|
|
|
el.$el = $(component.template); |
|
709
|
|
|
|
|
710
|
|
|
// Create all methods |
|
711
|
|
|
$.each(component.methods, function(name, method){ |
|
712
|
|
|
el[name] = function() {return method.apply(el,arguments);}; |
|
713
|
|
|
}); |
|
714
|
|
|
|
|
715
|
|
|
// Set properties |
|
716
|
|
|
$.each(component.props, function(name, prop){ |
|
717
|
|
|
el[name] = (typeof props !== 'undefined' && props.hasOwnProperty(name) ? props[name] : prop); |
|
718
|
|
|
}); |
|
719
|
|
|
|
|
720
|
|
|
// Bind events |
|
721
|
|
|
$.each(component.events, function(name, method){ |
|
722
|
|
|
el.$el.on(name, function() {return method.apply(el,arguments);}); |
|
723
|
|
|
}); |
|
724
|
|
|
|
|
725
|
|
|
// Call the 'created' function if exists |
|
726
|
|
|
if(component.hasOwnProperty('created')) component.created.call(el); |
|
|
|
|
|
|
727
|
|
|
|
|
728
|
|
|
return el; |
|
729
|
|
|
};Mivhak.component('caption', { |
|
730
|
|
|
template: '<div class="mivhak-caption"></div>', |
|
731
|
|
|
props: { |
|
732
|
|
|
text: null |
|
733
|
|
|
}, |
|
734
|
|
|
created: function() { |
|
735
|
|
|
this.setText(this.text); |
|
736
|
|
|
}, |
|
737
|
|
|
methods: { |
|
738
|
|
|
setText: function(text) { |
|
739
|
|
|
this.$el.html(text); |
|
740
|
|
|
} |
|
741
|
|
|
} |
|
742
|
|
|
});Mivhak.component('dropdown', { |
|
743
|
|
|
template: '<div class="mivhak-dropdown"></div>', |
|
744
|
|
|
props: { |
|
745
|
|
|
items: [], |
|
746
|
|
|
mivhakInstance: null, |
|
747
|
|
|
visible: false |
|
748
|
|
|
}, |
|
749
|
|
|
created: function() { |
|
750
|
|
|
var $this = this; |
|
751
|
|
|
$.each(this.items, function(i, item) { |
|
752
|
|
|
if( typeof item === 'string') item = Mivhak.buttons[item]; |
|
|
|
|
|
|
753
|
|
|
var button = $('<div>',{class: 'mivhak-dropdown-button', text: item.text, click: function(e){item.click.call($this.mivhakInstance,e);}}); |
|
754
|
|
|
if(item.toggle) |
|
755
|
|
|
{ |
|
756
|
|
|
button.$toggle = Mivhak.render('toggle'); |
|
757
|
|
|
|
|
758
|
|
|
// Toggle only if not clicking on the toggle itself (which makes it toggle as it is) |
|
759
|
|
|
button.click(function(e){if($(e.target).parents('.mivhak-dropdown-button').length !== 1)button.$toggle.toggle();}); |
|
|
|
|
|
|
760
|
|
|
button.append(button.$toggle.$el); |
|
761
|
|
|
} |
|
762
|
|
|
$this.$el.append(button); |
|
763
|
|
|
}); |
|
764
|
|
|
|
|
765
|
|
|
// Hide dropdown on outside click |
|
766
|
|
|
$(window).click(function(e){ |
|
767
|
|
|
if(!$(e.target).closest('.mivhak-icon-cog').length) { |
|
768
|
|
|
$this.$el.removeClass('mivhak-dropdown-visible'); |
|
769
|
|
|
} |
|
770
|
|
|
}); |
|
771
|
|
|
}, |
|
772
|
|
|
methods: { |
|
773
|
|
|
toggle: function() { |
|
774
|
|
|
this.visible = !this.visible; |
|
775
|
|
|
this.$el.toggleClass('mivhak-dropdown-visible'); |
|
776
|
|
|
} |
|
777
|
|
|
} |
|
778
|
|
|
});Mivhak.component('horizontal-scrollbar', { |
|
779
|
|
|
template: '<div class="mivhak-scrollbar mivhak-h-scrollbar"><div class="mivhak-scrollbar-thumb"></div></div>', |
|
780
|
|
|
props: { |
|
781
|
|
|
editor: null, |
|
782
|
|
|
$inner: null, |
|
783
|
|
|
$outer: null, |
|
784
|
|
|
mivhakInstance: null, |
|
785
|
|
|
minWidth: 50, |
|
786
|
|
|
state: { |
|
787
|
|
|
a: 0, // The total width of the editor |
|
788
|
|
|
b: 0, // The width of the viewport, excluding padding |
|
789
|
|
|
c: 0, // The width of the viewport, including padding |
|
790
|
|
|
d: 0, // The calculated width of the thumb |
|
791
|
|
|
l: 0 // The current left offset of the viewport |
|
792
|
|
|
}, |
|
793
|
|
|
initialized: false |
|
794
|
|
|
}, |
|
795
|
|
|
methods: { |
|
796
|
|
|
initialize: function() { |
|
797
|
|
|
if(!this.initialized) |
|
798
|
|
|
{ |
|
799
|
|
|
this.initialized = true; |
|
800
|
|
|
this.dragDealer(); |
|
801
|
|
|
var $this = this; |
|
802
|
|
|
$(window).resize(function(){ |
|
803
|
|
|
if(!$this.mivhakInstance.state.lineWrap) |
|
804
|
|
|
$this.refresh(); |
|
|
|
|
|
|
805
|
|
|
}); |
|
806
|
|
|
} |
|
807
|
|
|
this.refresh(); |
|
808
|
|
|
}, |
|
809
|
|
|
updateState: function() { |
|
810
|
|
|
var oldState = $.extend({}, this.state); |
|
811
|
|
|
this.state.a = this.getEditorWidth(); |
|
812
|
|
|
this.state.b = this.$outer.parent().width(); |
|
813
|
|
|
this.state.c = this.state.b - this.mivhakInstance.options.padding*2; |
|
814
|
|
|
this.state.d = Math.max(this.state.c*this.state.b/this.state.a,this.minWidth); |
|
815
|
|
|
this.state.l *= this.state.a/Math.max(oldState.a,1); // Math.max used to prevent division by zero |
|
816
|
|
|
return this.state.a !== oldState.a || this.state.b !== oldState.b; |
|
817
|
|
|
}, |
|
818
|
|
|
refresh: function() { |
|
819
|
|
|
var $this = this, oldLeft = this.state.l; |
|
820
|
|
|
raf(function(){ |
|
821
|
|
|
if($this.updateState()) |
|
822
|
|
|
{ |
|
823
|
|
|
if($this.getDifference() > 0) |
|
824
|
|
|
{ |
|
825
|
|
|
$this.doScroll('left',oldLeft-$this.state.l); |
|
826
|
|
|
$this.$el.css({width: $this.state.d + 'px', left: 0}); |
|
827
|
|
|
$this.moveBar(); |
|
828
|
|
|
} |
|
829
|
|
|
else |
|
830
|
|
|
{ |
|
831
|
|
|
$this.doScroll('left',$this.state.l); |
|
832
|
|
|
$this.$el.css({width: 0}); |
|
833
|
|
|
} |
|
834
|
|
|
} |
|
835
|
|
|
}); |
|
836
|
|
|
}, |
|
837
|
|
|
dragDealer: function(){ |
|
838
|
|
|
var $this = this, |
|
839
|
|
|
lastPageX; |
|
840
|
|
|
|
|
841
|
|
|
this.$el.on('mousedown.drag', function(e) { |
|
842
|
|
|
lastPageX = e.pageX; |
|
843
|
|
|
$this.$el.add(document.body).addClass('mivhak-scrollbar-grabbed'); |
|
844
|
|
|
$(document).on('mousemove.drag', drag).on('mouseup.drag', stop); |
|
845
|
|
|
return false; |
|
846
|
|
|
}); |
|
847
|
|
|
|
|
848
|
|
|
function drag(e){ |
|
849
|
|
|
var delta = e.pageX - lastPageX, |
|
850
|
|
|
didScroll; |
|
851
|
|
|
|
|
852
|
|
|
// Bail if the mouse hasn't moved |
|
853
|
|
|
if(!delta) return; |
|
|
|
|
|
|
854
|
|
|
|
|
855
|
|
|
lastPageX = e.pageX; |
|
856
|
|
|
|
|
857
|
|
|
raf(function(){ |
|
858
|
|
|
didScroll = $this.doScroll(delta > 0 ? 'right' : 'left', Math.abs(delta*$this.getEditorWidth()/$this.$outer.parent().width())); |
|
859
|
|
|
if(0 !== didScroll) $this.moveBar(); |
|
|
|
|
|
|
860
|
|
|
}); |
|
861
|
|
|
} |
|
862
|
|
|
|
|
863
|
|
|
function stop() { |
|
864
|
|
|
$this.$el.add(document.body).removeClass('mivhak-scrollbar-grabbed'); |
|
865
|
|
|
$(document).off("mousemove.drag mouseup.drag"); |
|
866
|
|
|
} |
|
867
|
|
|
}, |
|
868
|
|
|
moveBar: function() { |
|
869
|
|
|
this.$el.css({ |
|
870
|
|
|
left: (this.state.b-this.state.d)/(this.state.a-this.state.c)*this.state.l + 'px' |
|
871
|
|
|
}); |
|
872
|
|
|
}, |
|
873
|
|
|
|
|
874
|
|
|
/** |
|
875
|
|
|
* Scrolls the editor element in the direction given, provided that there |
|
876
|
|
|
* is remaining scroll space |
|
877
|
|
|
* @param {string} dir |
|
878
|
|
|
* @param {int} delta |
|
879
|
|
|
*/ |
|
880
|
|
|
doScroll: function(dir, delta) { |
|
881
|
|
|
var s = this.state, |
|
882
|
|
|
remaining, |
|
883
|
|
|
didScroll; |
|
884
|
|
|
|
|
885
|
|
|
if('left' === dir) |
|
886
|
|
|
{ |
|
887
|
|
|
remaining = s.l; |
|
888
|
|
|
didScroll = remaining > 0 ? Math.min(remaining,delta) : 0; |
|
889
|
|
|
s.l -= didScroll; |
|
890
|
|
|
} |
|
891
|
|
|
if('right' === dir) |
|
892
|
|
|
{ |
|
893
|
|
|
remaining = this.getDifference() - s.l; |
|
894
|
|
|
didScroll = remaining > 0 ? Math.min(remaining,delta) : 0; |
|
895
|
|
|
s.l += didScroll; |
|
896
|
|
|
} |
|
897
|
|
|
|
|
898
|
|
|
this.$inner.find('.ace_content').css({'margin-left': -s.l}); |
|
899
|
|
|
return didScroll; |
|
|
|
|
|
|
900
|
|
|
}, |
|
901
|
|
|
|
|
902
|
|
|
/** |
|
903
|
|
|
* Returns the difference between the containing div and the editor div |
|
904
|
|
|
*/ |
|
905
|
|
|
getDifference: function() |
|
906
|
|
|
{ |
|
907
|
|
|
return this.state.a - this.state.c; |
|
908
|
|
|
}, |
|
909
|
|
|
|
|
910
|
|
|
/** |
|
911
|
|
|
* Calculate the editor's width based on the number of lines |
|
912
|
|
|
*/ |
|
913
|
|
|
getEditorWidth: function() { |
|
914
|
|
|
return this.$inner.find('.ace_content').width(); |
|
915
|
|
|
} |
|
916
|
|
|
} |
|
917
|
|
|
});Mivhak.component('live-preview', { |
|
918
|
|
|
template: '<iframe class="mivhak-live-preview" allowtransparency="true" sandbox="allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-modals allow-forms" frameborder="0"></iframe>', |
|
919
|
|
|
props: { |
|
920
|
|
|
resources: [] |
|
921
|
|
|
}, |
|
922
|
|
|
methods: { |
|
923
|
|
|
renderHTML: function() { |
|
924
|
|
|
var html = '<html>', |
|
925
|
|
|
head = '<head>', |
|
926
|
|
|
body = '<body>'; |
|
927
|
|
|
|
|
928
|
|
|
head += '<meta http-equiv="content-type" content="text/html; charset=UTF-8">'; |
|
929
|
|
|
head += '<meta name="robots" content="noindex, nofollow">'; |
|
930
|
|
|
head += '<meta name="googlebot" content="noindex, nofollow">'; |
|
931
|
|
|
|
|
932
|
|
|
for(var i = 0; i < this.resources.count(); i++) |
|
933
|
|
|
{ |
|
934
|
|
|
var source = this.resources.get(i); |
|
935
|
|
|
if('markup' === source.runAs) body += source.content; |
|
|
|
|
|
|
936
|
|
|
if('style' === source.runAs) head += this.createStyle(source.content, source.visible ? false : source.source); |
|
|
|
|
|
|
937
|
|
|
if('script' === source.runAs) head += this.createScript(source.content, source.visible ? false : source.source); |
|
|
|
|
|
|
938
|
|
|
} |
|
939
|
|
|
|
|
940
|
|
|
html += head+'</head>'+body+'</body></html>'; |
|
941
|
|
|
|
|
942
|
|
|
return html; |
|
943
|
|
|
}, |
|
944
|
|
|
createScript: function(content,src) { |
|
945
|
|
|
if(src) return '<script src="'+src+'" type="text/javascript"></script>'; |
|
|
|
|
|
|
946
|
|
|
return '<script>\n//<![CDATA[\nwindow.onload = function(){'+content+'};//]]>\n</script>'; // @see http://stackoverflow.com/questions/66837/when-is-a-cdata-section-necessary-within-a-script-tag |
|
947
|
|
|
}, |
|
948
|
|
|
createStyle: function(content,href) { |
|
949
|
|
|
if(href) return '<link href="'+href+'" rel="stylesheet">'; |
|
|
|
|
|
|
950
|
|
|
return '<style>'+content+'</style>'; |
|
951
|
|
|
}, |
|
952
|
|
|
show: function() { |
|
953
|
|
|
this.$el.addClass('mivhak-active'); |
|
954
|
|
|
this.run(); |
|
955
|
|
|
}, |
|
956
|
|
|
hide: function() { |
|
957
|
|
|
this.$el.removeClass('mivhak-active'); |
|
958
|
|
|
}, |
|
959
|
|
|
run: function() { |
|
960
|
|
|
var contents = this.$el.contents(), |
|
961
|
|
|
doc = contents[0]; |
|
962
|
|
|
|
|
963
|
|
|
doc.open(); |
|
964
|
|
|
doc.writeln(this.renderHTML()); |
|
965
|
|
|
doc.close(); |
|
966
|
|
|
} |
|
967
|
|
|
} |
|
968
|
|
|
});Mivhak.component('notifier', { |
|
969
|
|
|
template: '<div class="mivhak-notifier"></div>', |
|
970
|
|
|
methods: { |
|
971
|
|
|
notification: function(html) { |
|
972
|
|
|
if(!html) return; |
|
|
|
|
|
|
973
|
|
|
clearTimeout(this.timeout); |
|
974
|
|
|
this.$el.off('click'); |
|
975
|
|
|
this.$el.html(html); |
|
976
|
|
|
this.$el.addClass('mivhak-visible'); |
|
977
|
|
|
}, |
|
978
|
|
|
timedNotification: function(html, timeout) { |
|
979
|
|
|
var $this = this; |
|
980
|
|
|
this.notification(html); |
|
981
|
|
|
this.timeout = setTimeout(function(){ |
|
982
|
|
|
$this.hide(); |
|
983
|
|
|
},timeout); |
|
984
|
|
|
}, |
|
985
|
|
|
closableNotification: function(html, onclick) |
|
986
|
|
|
{ |
|
987
|
|
|
var $this = this; |
|
988
|
|
|
this.notification(html); |
|
989
|
|
|
this.$el.addClass('mivhak-button'); |
|
990
|
|
|
this.$el.click(function(e){ |
|
991
|
|
|
$this.hide(); |
|
992
|
|
|
if(typeof onclick !== 'undefined') |
|
993
|
|
|
onclick.call(null, e); |
|
|
|
|
|
|
994
|
|
|
}); |
|
995
|
|
|
}, |
|
996
|
|
|
hide: function() { |
|
997
|
|
|
this.$el.removeClass('mivhak-visible mivhak-button'); |
|
998
|
|
|
} |
|
999
|
|
|
} |
|
1000
|
|
|
});Mivhak.component('tab-pane', { |
|
1001
|
|
|
template: '<div class="mivhak-tab-pane"><div class="mivhak-tab-pane-inner"></div></div>', |
|
1002
|
|
|
props: { |
|
1003
|
|
|
resource: null, |
|
1004
|
|
|
editor: null, |
|
1005
|
|
|
index: null, |
|
1006
|
|
|
padding: 10, |
|
1007
|
|
|
mivhakInstance: null |
|
1008
|
|
|
}, |
|
1009
|
|
|
created: function() { |
|
1010
|
|
|
this.setEditor(); |
|
1011
|
|
|
this.fetchRemoteSource(); |
|
1012
|
|
|
this.markLines(); |
|
1013
|
|
|
|
|
1014
|
|
|
this.$el = $(this.resource.pre).wrap(this.$el).parent().parent(); |
|
1015
|
|
|
this.$el.find('.mivhak-tab-pane-inner').css({margin: this.mivhakInstance.options.padding}); |
|
1016
|
|
|
this.setScrollbars(); |
|
1017
|
|
|
|
|
1018
|
|
|
}, |
|
1019
|
|
|
methods: { |
|
1020
|
|
|
getTheme: function() { |
|
1021
|
|
|
return this.mivhakInstance.options.theme === 'light' ? 'clouds' : 'ambiance'; |
|
1022
|
|
|
}, |
|
1023
|
|
|
fetchRemoteSource: function() { |
|
1024
|
|
|
var $this = this; |
|
1025
|
|
|
if(this.resource.source) { |
|
1026
|
|
|
$.ajax(this.resource.source).done(function(res){ |
|
1027
|
|
|
$this.editor.setValue(res,-1); |
|
1028
|
|
|
|
|
1029
|
|
|
// Refresh code viewer height |
|
1030
|
|
|
$this.mivhakInstance.callMethod('setHeight',$this.mivhakInstance.options.height); |
|
1031
|
|
|
|
|
1032
|
|
|
// Refresh scrollbars |
|
1033
|
|
|
raf(function(){ |
|
1034
|
|
|
$this.vscroll.refresh(); |
|
1035
|
|
|
$this.hscroll.refresh(); |
|
1036
|
|
|
}); |
|
1037
|
|
|
}); |
|
1038
|
|
|
|
|
1039
|
|
|
} |
|
1040
|
|
|
}, |
|
1041
|
|
|
setScrollbars: function() { |
|
1042
|
|
|
var $inner = $(this.resource.pre), |
|
1043
|
|
|
$outer = this.$el.find('.mivhak-tab-pane-inner'); |
|
1044
|
|
|
|
|
1045
|
|
|
this.vscroll = Mivhak.render('vertical-scrollbar',{editor: this.editor, $inner: $inner, $outer: $outer, mivhakInstance: this.mivhakInstance}); |
|
1046
|
|
|
this.hscroll = Mivhak.render('horizontal-scrollbar',{editor: this.editor, $inner: $inner, $outer: $outer, mivhakInstance: this.mivhakInstance}); |
|
1047
|
|
|
|
|
1048
|
|
|
this.$el.append(this.vscroll.$el, this.hscroll.$el); |
|
1049
|
|
|
}, |
|
1050
|
|
|
show: function() { |
|
1051
|
|
|
this.$el.addClass('mivhak-tab-pane-active'); |
|
1052
|
|
|
this.editor.focus(); |
|
1053
|
|
|
this.editor.gotoLine(0); // Needed in order to get focus |
|
1054
|
|
|
|
|
1055
|
|
|
// Recalculate scrollbar positions based on the now visible element |
|
1056
|
|
|
this.vscroll.initialize(); |
|
1057
|
|
|
this.hscroll.initialize(); |
|
1058
|
|
|
}, |
|
1059
|
|
|
hide: function() { |
|
1060
|
|
|
this.$el.removeClass('mivhak-tab-pane-active'); |
|
1061
|
|
|
}, |
|
1062
|
|
|
setEditor: function() { |
|
1063
|
|
|
|
|
1064
|
|
|
// Remove redundant space from code |
|
1065
|
|
|
this.resource.pre.textContent = this.resource.pre.textContent.trim(); |
|
1066
|
|
|
|
|
1067
|
|
|
// Set editor options |
|
1068
|
|
|
this.editor = ace.edit(this.resource.pre); |
|
|
|
|
|
|
1069
|
|
|
this.editor.setReadOnly(!this.mivhakInstance.options.editable); |
|
1070
|
|
|
this.editor.setTheme("ace/theme/"+this.getTheme()); |
|
1071
|
|
|
this.editor.setShowPrintMargin(false); |
|
1072
|
|
|
this.editor.renderer.setShowGutter(this.mivhakInstance.options.lineNumbers); |
|
1073
|
|
|
this.editor.getSession().setMode("ace/mode/"+this.resource.lang); |
|
1074
|
|
|
this.editor.getSession().setUseWorker(false); // Disable syntax checking |
|
1075
|
|
|
this.editor.getSession().setUseWrapMode(true); // Set initial line wrapping |
|
1076
|
|
|
|
|
1077
|
|
|
this.editor.setOptions({ |
|
1078
|
|
|
maxLines: Infinity, |
|
1079
|
|
|
firstLineNumber: this.resource.startLine, |
|
1080
|
|
|
highlightActiveLine: false, |
|
1081
|
|
|
fontSize: parseInt(14) |
|
1082
|
|
|
}); |
|
1083
|
|
|
|
|
1084
|
|
|
// Update source content for the live preview |
|
1085
|
|
|
if(this.mivhakInstance.options.editable) |
|
1086
|
|
|
{ |
|
1087
|
|
|
var $this = this; |
|
1088
|
|
|
this.editor.getSession().on('change', function(a,b,c) { |
|
|
|
|
|
|
1089
|
|
|
$this.mivhakInstance.resources.update($this.index, $this.editor.getValue()); |
|
1090
|
|
|
}); |
|
1091
|
|
|
} |
|
1092
|
|
|
}, |
|
1093
|
|
|
markLines: function() |
|
1094
|
|
|
{ |
|
1095
|
|
|
if(!this.resource.mark) return; |
|
|
|
|
|
|
1096
|
|
|
var ranges = strToRange(this.resource.mark), |
|
1097
|
|
|
i = ranges.length, |
|
1098
|
|
|
AceRange = ace.require("ace/range").Range; |
|
|
|
|
|
|
1099
|
|
|
|
|
1100
|
|
|
while(i--) |
|
1101
|
|
|
{ |
|
1102
|
|
|
this.editor.session.addMarker( |
|
1103
|
|
|
new AceRange(ranges[i].start, 0, ranges[i].end, 1), // Define the range of the marker |
|
1104
|
|
|
"ace_active-line", // Set the CSS class for the marker |
|
1105
|
|
|
"fullLine" // Marker type |
|
1106
|
|
|
); |
|
1107
|
|
|
} |
|
1108
|
|
|
} |
|
1109
|
|
|
} |
|
1110
|
|
|
});Mivhak.component('tabs', { |
|
1111
|
|
|
template: '<div class="mivhak-tabs"></div>', |
|
1112
|
|
|
props: { |
|
1113
|
|
|
mivhakInstance: null, |
|
1114
|
|
|
activeTab: null, |
|
1115
|
|
|
tabs: [] |
|
1116
|
|
|
}, |
|
1117
|
|
|
created: function() { |
|
1118
|
|
|
var $this = this; |
|
1119
|
|
|
this.$el = this.mivhakInstance.$selection.find('pre').wrapAll(this.$el).parent(); |
|
1120
|
|
|
$.each(this.mivhakInstance.resources.data,function(i, resource){ |
|
1121
|
|
|
if(resource.visible) |
|
1122
|
|
|
$this.tabs.push(Mivhak.render('tab-pane',{ |
|
|
|
|
|
|
1123
|
|
|
resource: resource, |
|
1124
|
|
|
index: i, |
|
1125
|
|
|
mivhakInstance: $this.mivhakInstance |
|
1126
|
|
|
})); |
|
1127
|
|
|
}); |
|
1128
|
|
|
}, |
|
1129
|
|
|
methods: { |
|
1130
|
|
|
showTab: function(index){ |
|
1131
|
|
|
var $this = this; |
|
1132
|
|
|
$.each(this.tabs, function(i, tab){ |
|
1133
|
|
|
if(index === i) { |
|
1134
|
|
|
$this.mivhakInstance.activeTab = tab; |
|
1135
|
|
|
tab.show(); |
|
1136
|
|
|
} |
|
1137
|
|
|
else tab.hide(); |
|
|
|
|
|
|
1138
|
|
|
}); |
|
1139
|
|
|
} |
|
1140
|
|
|
} |
|
1141
|
|
|
});Mivhak.component('toggle', { |
|
1142
|
|
|
template: '<div class="mivhak-toggle"><div class="mivhak-toggle-knob"></div></div>', |
|
1143
|
|
|
props: { |
|
1144
|
|
|
on: true |
|
1145
|
|
|
}, |
|
1146
|
|
|
events: { |
|
1147
|
|
|
click: function() { |
|
1148
|
|
|
this.toggle(); |
|
1149
|
|
|
} |
|
1150
|
|
|
}, |
|
1151
|
|
|
created: function() { |
|
1152
|
|
|
this.$el.addClass('mivhak-toggle-'+(this.on?'on':'off')); |
|
1153
|
|
|
}, |
|
1154
|
|
|
methods: { |
|
1155
|
|
|
toggle: function() { |
|
1156
|
|
|
this.on = !this.on; |
|
1157
|
|
|
this.$el.toggleClass('mivhak-toggle-on').toggleClass('mivhak-toggle-off'); |
|
1158
|
|
|
} |
|
1159
|
|
|
} |
|
1160
|
|
|
});Mivhak.component('top-bar-button', { |
|
1161
|
|
|
template: '<div class="mivhak-top-bar-button"></div>', |
|
1162
|
|
|
props: { |
|
1163
|
|
|
text: null, |
|
1164
|
|
|
icon: null, |
|
1165
|
|
|
dropdown: null, |
|
1166
|
|
|
mivhakInstance: null, |
|
1167
|
|
|
onClick: function(){} |
|
1168
|
|
|
}, |
|
1169
|
|
|
events: { |
|
1170
|
|
|
click: function() { |
|
1171
|
|
|
this.onClick(); |
|
1172
|
|
|
} |
|
1173
|
|
|
}, |
|
1174
|
|
|
created: function() { |
|
1175
|
|
|
var $this = this; |
|
1176
|
|
|
this.$el.text(this.text); |
|
1177
|
|
|
if(this.icon) this.$el.addClass('mivhak-icon mivhak-icon-'+this.icon).append($(Mivhak.icons[this.icon])); |
|
|
|
|
|
|
1178
|
|
|
if(this.dropdown) |
|
1179
|
|
|
{ |
|
1180
|
|
|
$this.$el.append(this.dropdown.$el); |
|
1181
|
|
|
this.onClick = function() { |
|
1182
|
|
|
$this.toggleActivation(); |
|
1183
|
|
|
$this.dropdown.toggle(); |
|
1184
|
|
|
}; |
|
1185
|
|
|
} |
|
1186
|
|
|
}, |
|
1187
|
|
|
methods: { |
|
1188
|
|
|
activate: function() { |
|
1189
|
|
|
this.$el.addClass('mivhak-button-active'); |
|
1190
|
|
|
}, |
|
1191
|
|
|
deactivate: function() { |
|
1192
|
|
|
this.$el.removeClass('mivhak-button-active'); |
|
1193
|
|
|
}, |
|
1194
|
|
|
toggleActivation: function() { |
|
1195
|
|
|
this.$el.toggleClass('mivhak-button-active'); |
|
1196
|
|
|
}, |
|
1197
|
|
|
isActive: function() { |
|
1198
|
|
|
return this.$el.hasClass('mivhak-button-active'); |
|
1199
|
|
|
} |
|
1200
|
|
|
} |
|
1201
|
|
|
});Mivhak.component('top-bar', { |
|
1202
|
|
|
template: '<div class="mivhak-top-bar"><div class="mivhak-nav-tabs"></div><div class="mivhak-controls"></div><div class="mivhak-line"></div></div>', |
|
1203
|
|
|
props: { |
|
1204
|
|
|
mivhakInstance: null, |
|
1205
|
|
|
navTabs: [], |
|
1206
|
|
|
controls: [], |
|
1207
|
|
|
line: null |
|
1208
|
|
|
}, |
|
1209
|
|
|
created: function() { |
|
1210
|
|
|
this.line = this.$el.find('.mivhak-line'); |
|
1211
|
|
|
this.createTabNav(); |
|
1212
|
|
|
if(this.mivhakInstance.options.runnable) this.createPlayButton(); |
|
|
|
|
|
|
1213
|
|
|
this.createCogButton(); |
|
1214
|
|
|
}, |
|
1215
|
|
|
methods: { |
|
1216
|
|
|
activateNavTab: function(index) { |
|
1217
|
|
|
var button = this.navTabs[index]; |
|
1218
|
|
|
// Deactivate all tabs and activate this tab |
|
1219
|
|
|
$.each(this.navTabs, function(i,navTab){navTab.deactivate();}); |
|
1220
|
|
|
button.activate(); |
|
1221
|
|
|
|
|
1222
|
|
|
// Position the line |
|
1223
|
|
|
this.moveLine(button.$el); |
|
1224
|
|
|
}, |
|
1225
|
|
|
moveLine: function($el) { |
|
1226
|
|
|
if(typeof $el === 'undefined') { |
|
1227
|
|
|
this.line.removeClass('mivhak-visible'); |
|
1228
|
|
|
return; |
|
1229
|
|
|
} |
|
1230
|
|
|
this.line.width($el.width()); |
|
1231
|
|
|
this.line.css({left:$el.position().left + ($el.outerWidth() - $el.width())/2}); |
|
1232
|
|
|
this.line.addClass('mivhak-visible'); |
|
1233
|
|
|
}, |
|
1234
|
|
|
createTabNav: function() { |
|
1235
|
|
|
var source, i, pos = 0; |
|
1236
|
|
|
for(i = 0; i < this.mivhakInstance.resources.count(); i++) |
|
1237
|
|
|
{ |
|
1238
|
|
|
source = this.mivhakInstance.resources.get(i); |
|
1239
|
|
|
if(source.visible) this.createNavTabButton(pos++, source.lang); |
|
|
|
|
|
|
1240
|
|
|
} |
|
1241
|
|
|
}, |
|
1242
|
|
|
createNavTabButton: function(i, lang) { |
|
1243
|
|
|
var $this = this, |
|
1244
|
|
|
button = Mivhak.render('top-bar-button',{ |
|
1245
|
|
|
text: lang, |
|
1246
|
|
|
onClick: function() { |
|
1247
|
|
|
$this.mivhakInstance.callMethod('showTab',i); |
|
1248
|
|
|
} |
|
1249
|
|
|
}); |
|
1250
|
|
|
this.navTabs.push(button); |
|
1251
|
|
|
this.$el.find('.mivhak-nav-tabs').append(button.$el); |
|
1252
|
|
|
}, |
|
1253
|
|
|
createPlayButton: function() { |
|
1254
|
|
|
var $this = this; |
|
1255
|
|
|
var playBtn = Mivhak.render('top-bar-button',{ |
|
1256
|
|
|
icon: 'play', |
|
1257
|
|
|
onClick: function() { |
|
1258
|
|
|
$this.mivhakInstance.preview.show(); |
|
1259
|
|
|
$this.moveLine(); |
|
1260
|
|
|
} |
|
1261
|
|
|
}); |
|
1262
|
|
|
this.controls.push(playBtn); |
|
1263
|
|
|
this.$el.find('.mivhak-controls').append(playBtn.$el); |
|
1264
|
|
|
}, |
|
1265
|
|
|
createCogButton: function() { |
|
1266
|
|
|
var cogBtn = Mivhak.render('top-bar-button',{ |
|
1267
|
|
|
icon: 'cog', |
|
1268
|
|
|
mivhakInstance: this.mivhakInstance, |
|
1269
|
|
|
dropdown: Mivhak.render('dropdown',{ |
|
1270
|
|
|
mivhakInstance: this.mivhakInstance, |
|
1271
|
|
|
items: this.mivhakInstance.options.buttons |
|
1272
|
|
|
}) |
|
1273
|
|
|
}); |
|
1274
|
|
|
this.controls.push(cogBtn); |
|
1275
|
|
|
this.$el.find('.mivhak-controls').append(cogBtn.$el); |
|
1276
|
|
|
} |
|
1277
|
|
|
} |
|
1278
|
|
|
});Mivhak.component('vertical-scrollbar', { |
|
1279
|
|
|
template: '<div class="mivhak-scrollbar mivhak-v-scrollbar"><div class="mivhak-scrollbar-thumb"></div></div>', |
|
1280
|
|
|
props: { |
|
1281
|
|
|
editor: null, |
|
1282
|
|
|
$inner: null, |
|
1283
|
|
|
$outer: null, |
|
1284
|
|
|
mivhakInstance: null, |
|
1285
|
|
|
minHeight: 50, |
|
1286
|
|
|
state: { |
|
1287
|
|
|
a: 0, // The total height of the editor |
|
1288
|
|
|
b: 0, // The height of the viewport, excluding padding |
|
1289
|
|
|
c: 0, // The height of the viewport, including padding |
|
1290
|
|
|
d: 0, // The calculated thumb height |
|
1291
|
|
|
t: 0 // The current top offset of the viewport |
|
1292
|
|
|
}, |
|
1293
|
|
|
initialized: false |
|
1294
|
|
|
}, |
|
1295
|
|
|
methods: { |
|
1296
|
|
|
initialize: function() { |
|
1297
|
|
|
if(!this.initialized) |
|
1298
|
|
|
{ |
|
1299
|
|
|
this.initialized = true; |
|
1300
|
|
|
this.dragDealer(); |
|
1301
|
|
|
var $this = this; |
|
1302
|
|
|
this.$inner.on('mousewheel', function(e){$this.onScroll.call(this, e);}); |
|
1303
|
|
|
$(window).resize(function(){ |
|
1304
|
|
|
if($this.mivhakInstance.state.lineWrap) |
|
1305
|
|
|
$this.refresh(); |
|
|
|
|
|
|
1306
|
|
|
}); |
|
1307
|
|
|
} |
|
1308
|
|
|
// Refresh anytime initialize is called |
|
1309
|
|
|
this.refresh(); |
|
1310
|
|
|
}, |
|
1311
|
|
|
updateState: function() { |
|
1312
|
|
|
var oldState = $.extend({}, this.state); |
|
1313
|
|
|
this.state.a = getEditorHeight(this.$inner); |
|
1314
|
|
|
this.state.b = this.mivhakInstance.state.height; |
|
1315
|
|
|
this.state.c = this.mivhakInstance.state.height-this.mivhakInstance.options.padding*2; |
|
1316
|
|
|
this.state.d = Math.max(this.state.c*this.state.b/this.state.a,this.minHeight); |
|
1317
|
|
|
this.state.t *= this.state.a/Math.max(oldState.a,1); // Math.max used to prevent division by zero |
|
1318
|
|
|
return this.state.a !== oldState.a || this.state.b !== oldState.b; |
|
1319
|
|
|
}, |
|
1320
|
|
|
refresh: function() { |
|
1321
|
|
|
var $this = this, oldTop = this.state.t; |
|
1322
|
|
|
raf(function(){ |
|
1323
|
|
|
if($this.updateState()) |
|
1324
|
|
|
{ |
|
1325
|
|
|
if($this.getDifference() > 0) |
|
1326
|
|
|
{ |
|
1327
|
|
|
$this.doScroll('up',oldTop-$this.state.t); |
|
1328
|
|
|
$this.$el.css({height: $this.state.d + 'px', top: 0}); |
|
1329
|
|
|
$this.moveBar(); |
|
1330
|
|
|
} |
|
1331
|
|
|
else |
|
1332
|
|
|
{ |
|
1333
|
|
|
$this.doScroll('up',$this.state.t); |
|
1334
|
|
|
$this.$el.css({height: 0}); |
|
1335
|
|
|
} |
|
1336
|
|
|
} |
|
1337
|
|
|
}); |
|
1338
|
|
|
}, |
|
1339
|
|
|
onScroll: function(e) { |
|
1340
|
|
|
var didScroll; |
|
1341
|
|
|
|
|
1342
|
|
|
if(e.deltaY > 0) |
|
1343
|
|
|
didScroll = this.doScroll('up',e.deltaY*e.deltaFactor); |
|
|
|
|
|
|
1344
|
|
|
else |
|
1345
|
|
|
didScroll = this.doScroll('down',-e.deltaY*e.deltaFactor); |
|
1346
|
|
|
|
|
1347
|
|
|
if(0 !== didScroll) { |
|
1348
|
|
|
this.moveBar(); |
|
1349
|
|
|
e.preventDefault(); // Only prevent page scroll if the editor can be scrolled |
|
1350
|
|
|
} |
|
1351
|
|
|
}, |
|
1352
|
|
|
dragDealer: function(){ |
|
1353
|
|
|
var $this = this, |
|
1354
|
|
|
lastPageY; |
|
1355
|
|
|
|
|
1356
|
|
|
this.$el.on('mousedown.drag', function(e) { |
|
1357
|
|
|
lastPageY = e.pageY; |
|
1358
|
|
|
$this.$el.add(document.body).addClass('mivhak-scrollbar-grabbed'); |
|
1359
|
|
|
$(document).on('mousemove.drag', drag).on('mouseup.drag', stop); |
|
1360
|
|
|
return false; |
|
1361
|
|
|
}); |
|
1362
|
|
|
|
|
1363
|
|
|
function drag(e){ |
|
1364
|
|
|
var delta = e.pageY - lastPageY, |
|
1365
|
|
|
didScroll; |
|
1366
|
|
|
|
|
1367
|
|
|
// Bail if the mouse hasn't moved |
|
1368
|
|
|
if(!delta) return; |
|
|
|
|
|
|
1369
|
|
|
|
|
1370
|
|
|
lastPageY = e.pageY; |
|
1371
|
|
|
|
|
1372
|
|
|
raf(function(){ |
|
1373
|
|
|
didScroll = $this.doScroll(delta > 0 ? 'down' : 'up', Math.abs(delta*getEditorHeight($this.$inner)/$this.$outer.parent().height())); |
|
1374
|
|
|
if(0 !== didScroll) $this.moveBar(); |
|
|
|
|
|
|
1375
|
|
|
}); |
|
1376
|
|
|
} |
|
1377
|
|
|
|
|
1378
|
|
|
function stop() { |
|
1379
|
|
|
$this.$el.add(document.body).removeClass('mivhak-scrollbar-grabbed'); |
|
1380
|
|
|
$(document).off("mousemove.drag mouseup.drag"); |
|
1381
|
|
|
} |
|
1382
|
|
|
}, |
|
1383
|
|
|
moveBar: function() { |
|
1384
|
|
|
this.$el.css({ |
|
1385
|
|
|
top: (this.state.b-this.state.d)/(this.state.a-this.state.c)*this.state.t + 'px' |
|
1386
|
|
|
}); |
|
1387
|
|
|
}, |
|
1388
|
|
|
|
|
1389
|
|
|
/** |
|
1390
|
|
|
* Scrolls the editor element in the direction given, provided that there |
|
1391
|
|
|
* is remaining scroll space |
|
1392
|
|
|
* @param {string} dir |
|
1393
|
|
|
* @param {int} delta |
|
1394
|
|
|
*/ |
|
1395
|
|
|
doScroll: function(dir, delta) { |
|
1396
|
|
|
var s = this.state, |
|
1397
|
|
|
remaining, |
|
1398
|
|
|
didScroll; |
|
1399
|
|
|
|
|
1400
|
|
|
if('up' === dir) |
|
1401
|
|
|
{ |
|
1402
|
|
|
remaining = s.t; |
|
1403
|
|
|
didScroll = remaining > 0 ? Math.min(remaining,delta) : 0; |
|
1404
|
|
|
s.t -= didScroll; |
|
1405
|
|
|
} |
|
1406
|
|
|
if('down' === dir) |
|
1407
|
|
|
{ |
|
1408
|
|
|
remaining = this.getDifference() - s.t; |
|
1409
|
|
|
didScroll = remaining > 0 ? Math.min(remaining,delta) : 0; |
|
1410
|
|
|
s.t += didScroll; |
|
1411
|
|
|
} |
|
1412
|
|
|
|
|
1413
|
|
|
this.$inner.css({top: -s.t}); |
|
1414
|
|
|
return didScroll; |
|
|
|
|
|
|
1415
|
|
|
}, |
|
1416
|
|
|
|
|
1417
|
|
|
/** |
|
1418
|
|
|
* Returns the difference between the containing div and the editor div |
|
1419
|
|
|
*/ |
|
1420
|
|
|
getDifference: function() |
|
1421
|
|
|
{ |
|
1422
|
|
|
return this.state.a - this.state.c; |
|
1423
|
|
|
} |
|
1424
|
|
|
} |
|
1425
|
|
|
});/** |
|
1426
|
|
|
* Extends the functionality of jQuery to include Mivhak |
|
1427
|
|
|
* |
|
1428
|
|
|
* @param {Function|Object} methodOrOptions |
|
1429
|
|
|
* @returns {jQuery} |
|
1430
|
|
|
*/ |
|
1431
|
|
|
$.fn.mivhak = function( methodOrOptions ) { |
|
1432
|
|
|
|
|
1433
|
|
|
// Store arguments for use with methods |
|
1434
|
|
|
var args = arguments.length > 1 ? Array.apply(null, arguments).slice(1) : null; |
|
1435
|
|
|
|
|
1436
|
|
|
return this.each(function(){ |
|
1437
|
|
|
|
|
1438
|
|
|
// If this is an options object, set or update the options |
|
1439
|
|
|
if( typeof methodOrOptions === 'object' || !methodOrOptions ) |
|
1440
|
|
|
{ |
|
1441
|
|
|
// If this is the initial call for this element, instantiate a new Mivhak object |
|
1442
|
|
|
if( typeof $(this).data( 'mivhak' ) === 'undefined' ) { |
|
1443
|
|
|
var plugin = new Mivhak( this, methodOrOptions ); |
|
1444
|
|
|
$(this).data( 'mivhak', plugin ); |
|
1445
|
|
|
} |
|
1446
|
|
|
// Otherwise update existing settings (consequent calls will update, rather than recreate Mivhak) |
|
1447
|
|
|
else |
|
1448
|
|
|
{ |
|
1449
|
|
|
$(this).data('mivhak').setOptions( methodOrOptions ); |
|
1450
|
|
|
$(this).data('mivhak').applyOptions(); |
|
1451
|
|
|
} |
|
1452
|
|
|
} |
|
1453
|
|
|
|
|
1454
|
|
|
// If this is a method call, run the method (if it exists) |
|
1455
|
|
|
else if( Mivhak.methodExists( methodOrOptions ) ) |
|
1456
|
|
|
{ |
|
1457
|
|
|
Mivhak.methods[methodOrOptions].apply($(this).data('mivhak'), args); |
|
1458
|
|
|
} |
|
1459
|
|
|
}); |
|
1460
|
|
|
};}( jQuery )); |