Code Duplication    Length = 1434-1449 lines in 2 locations

build/mivhak-dev.js 1 location

@@ 12-1460 (lines=1449) @@
9
 * Developed by Askupa Software http://www.askupasoftware.com
10
 */
11
/* test-code */
12
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 &#8984;+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 ));

build/mivhak.js 1 location

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