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