Total Complexity | 232 |
Complexity/F | 1.65 |
Lines of Code | 1434 |
Function Count | 141 |
Duplicated Lines | 1434 |
Ratio | 100 % |
Changes | 1 | ||
Bugs | 0 | Features | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like build/mivhak.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | /** |
||
12 | View Code Duplication | (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 ⌘+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 )); |