@@ 12-1460 (lines=1449) @@ | ||
9 | * Developed by Askupa Software http://www.askupasoftware.com |
|
10 | */ |
|
11 | /* test-code */ |
|
12 | var testapi = {}; |
|
13 | /* end-test-code */ |
|
14 | ||
15 | (function ( $ ) {// Ace global config |
|
16 | ace.config.set('basePath', 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.3/');/** |
|
17 | * Converts a string to it's actual value, if applicable |
|
18 | * |
|
19 | * @param {String} str |
|
20 | */ |
|
21 | function strToValue( str ) |
|
22 | { |
|
23 | if('true' === str.toLowerCase()) return true; |
|
24 | if('false' === str.toLowerCase()) return false; |
|
25 | if(!isNaN(str)) return parseFloat(str); |
|
26 | return str; |
|
27 | } |
|
28 | ||
29 | /** |
|
30 | * Convert hyphened text to camelCase. |
|
31 | * |
|
32 | * @param {string} str |
|
33 | * @returns {string} |
|
34 | */ |
|
35 | function toCamelCase( str ) |
|
36 | { |
|
37 | return str.replace(/-(.)/g,function(match){ |
|
38 | return match[1].toUpperCase(); |
|
39 | }); |
|
40 | } |
|
41 | ||
42 | /** |
|
43 | * Reads the element's 'miv-' attributes and returns their values as an object |
|
44 | * |
|
45 | * @param {DOMElement} el |
|
46 | * @returns {Object} |
|
47 | */ |
|
48 | function readAttributes( el ) |
|
49 | { |
|
50 | var options = {}; |
|
51 | $.each(el.attributes, function(i, attr){ |
|
52 | if(/^miv-/.test(attr.name)) |
|
53 | { |
|
54 | options[toCamelCase(attr.name.substr(4))] = strToValue(attr.value); |
|
55 | } |
|
56 | }); |
|
57 | return options; |
|
58 | } |
|
59 | ||
60 | /** |
|
61 | * Get the average value of all elements in the given array. |
|
62 | * |
|
63 | * @param {Array} arr |
|
64 | * @returns {Number} |
|
65 | */ |
|
66 | function average( arr ) |
|
67 | { |
|
68 | var i = arr.length, sum = 0; |
|
69 | while(i--) sum += parseFloat(arr[i]); |
|
70 | return sum/arr.length; |
|
71 | } |
|
72 | ||
73 | /** |
|
74 | * Get the maximum value of all elements in the given array. |
|
75 | * |
|
76 | * @param {Array} arr |
|
77 | * @returns {Number} |
|
78 | */ |
|
79 | function max( arr ) |
|
80 | { |
|
81 | var i = arr.length, maxval = arr[--i]; |
|
82 | while(i--) if(arr[i] > maxval) maxval = arr[i]; |
|
83 | return maxval; |
|
84 | } |
|
85 | ||
86 | /** |
|
87 | * Get the minimum value of all elements in the given array. |
|
88 | * |
|
89 | * @param {Array} arr |
|
90 | * @returns {Number} |
|
91 | */ |
|
92 | function min( arr ) |
|
93 | { |
|
94 | var i = arr.length, minval = arr[--i]; |
|
95 | while(i--) if(arr[i] < minval) minval = arr[i]; |
|
96 | return minval; |
|
97 | } |
|
98 | ||
99 | /** |
|
100 | * Calculate the editor's height based on the number of lines & line height. |
|
101 | * |
|
102 | * @param {jQuery} $editor Ther editor wrapper element (PRE) |
|
103 | * @returns {Number} |
|
104 | */ |
|
105 | function getEditorHeight( $editor ) |
|
106 | { |
|
107 | var height = 0; |
|
108 | $editor.find('.ace_text-layer').children().each(function(){ |
|
109 | height += $(this).height(); |
|
110 | }); |
|
111 | return height; |
|
112 | } |
|
113 | ||
114 | /** |
|
115 | * Convert a string like "3, 5-7" into an array of ranges in to form of |
|
116 | * [ |
|
117 | * {start:2, end:2}, |
|
118 | * {start:4, end:6}, |
|
119 | * ] |
|
120 | * The string should be given as a list if comma delimited 1 based ranges. |
|
121 | * The result is given as a 0 based array of ranges. |
|
122 | * |
|
123 | * @param {string} str |
|
124 | * @returns {Array} |
|
125 | */ |
|
126 | function strToRange( str ) |
|
127 | { |
|
128 | var range = str.replace(' ', '').split(','), |
|
129 | i = range.length, |
|
130 | ranges = [], |
|
131 | start, end, splitted; |
|
132 | ||
133 | while(i--) |
|
134 | { |
|
135 | // Multiple lines highlight |
|
136 | if( range[i].indexOf('-') > -1 ) |
|
137 | { |
|
138 | splitted = range[i].split('-'); |
|
139 | start = parseInt(splitted[0])-1; |
|
140 | end = parseInt(splitted[1])-1; |
|
141 | } |
|
142 | ||
143 | // Single line highlight |
|
144 | else |
|
145 | { |
|
146 | start = parseInt(range[i])-1; |
|
147 | end = start; |
|
148 | } |
|
149 | ||
150 | ranges.unshift({start:start,end:end}); |
|
151 | } |
|
152 | ||
153 | return ranges; |
|
154 | } |
|
155 | ||
156 | /** |
|
157 | * Request animation frame. Uses setTimeout as a fallback if the browser does |
|
158 | * not support requestAnimationFrame (based on 60 frames per second). |
|
159 | * |
|
160 | * @param {type} cb |
|
161 | * @returns {Number} |
|
162 | */ |
|
163 | var raf = window.requestAnimationFrame || |
|
164 | window.webkitRequestAnimationFrame || |
|
165 | window.mozRequestAnimationFrame || |
|
166 | window.msRequestAnimationFrame || |
|
167 | function(cb) { return window.setTimeout(cb, 1000 / 60); }; |
|
168 | ||
169 | /* test-code */ |
|
170 | testapi.strToValue = strToValue; |
|
171 | testapi.toCamelCase = toCamelCase; |
|
172 | testapi.readAttributes = readAttributes; |
|
173 | testapi.average = average; |
|
174 | testapi.max = max; |
|
175 | testapi.min = min; |
|
176 | testapi.getEditorHeight = getEditorHeight; |
|
177 | testapi.strToRange = strToRange; |
|
178 | testapi.raf = raf; |
|
179 | /* end-test-code *//** |
|
180 | * The constructor. |
|
181 | * See Mivhal.defaults for available options. |
|
182 | * |
|
183 | * @param {DOMElement} selection |
|
184 | * @param {Object} options |
|
185 | */ |
|
186 | function Mivhak( selection, options ) |
|
187 | { |
|
188 | // Bail if there are no resources |
|
189 | if(!selection.getElementsByTagName('PRE').length) return; |
|
190 | ||
191 | this.$selection = $( selection ); |
|
192 | this.setOptions( options ); |
|
193 | this.init(); |
|
194 | } |
|
195 | ||
196 | /** |
|
197 | * Check if a given string represents a supported method |
|
198 | * @param {string} method |
|
199 | */ |
|
200 | Mivhak.methodExists = function( method ) |
|
201 | { |
|
202 | return typeof method === 'string' && Mivhak.methods[method]; |
|
203 | }; |
|
204 | ||
205 | /** |
|
206 | * Initiate the code viewer. |
|
207 | */ |
|
208 | Mivhak.prototype.init = function() |
|
209 | { |
|
210 | this.initState(); |
|
211 | this.parseResources(); |
|
212 | this.createUI(); |
|
213 | this.applyOptions(); |
|
214 | this.callMethod('showTab',0); // Show first tab initially |
|
215 | }; |
|
216 | ||
217 | /** |
|
218 | * Apply the options that were set by the user. This function is called when |
|
219 | * Mivhak is initiated, and every time the options are updated. |
|
220 | */ |
|
221 | Mivhak.prototype.applyOptions = function() |
|
222 | { |
|
223 | this.callMethod('setHeight', this.options.height); |
|
224 | this.callMethod('setAccentColor', this.options.accentColor); |
|
225 | if(this.options.collapsed) this.callMethod('collapse'); |
|
226 | if(!this.options.topbar) this.$selection.addClass('mivhak-no-topbar'); |
|
227 | else this.$selection.removeClass('mivhak-no-topbar'); |
|
228 | ||
229 | this.createCaption(); |
|
230 | this.createLivePreview(); |
|
231 | }; |
|
232 | ||
233 | /** |
|
234 | * Initiate this instance's state. |
|
235 | */ |
|
236 | Mivhak.prototype.initState = function() |
|
237 | { |
|
238 | this.state = { |
|
239 | lineWrap: true, |
|
240 | collapsed: false, |
|
241 | height: 0, |
|
242 | activeTab: null, // Updated by tabs.showTab |
|
243 | resources: [] // Generated by parseResources() |
|
244 | }; |
|
245 | }; |
|
246 | ||
247 | /** |
|
248 | * Set or update this instance's options. |
|
249 | * @param {object} options |
|
250 | */ |
|
251 | Mivhak.prototype.setOptions = function( options ) |
|
252 | { |
|
253 | // If options were already set, update them |
|
254 | if( typeof this.options !== 'undefined' ) |
|
255 | this.options = $.extend(true, {}, this.options, options, readAttributes(this.$selection[0])); |
|
256 | ||
257 | // Otherwise, merge them with the defaults |
|
258 | else this.options = $.extend(true, {}, Mivhak.defaults, options, readAttributes(this.$selection[0])); |
|
259 | }; |
|
260 | ||
261 | /** |
|
262 | * Call one of Mivhak's methods. See Mivhak.methods for available methods. |
|
263 | * To apply additional arguments, simply pass the arguments after the methodName |
|
264 | * i.e. callMethod('methodName', arg1, arg2). |
|
265 | * This method is also called internally when making a method call through jQuery |
|
266 | * i.e. $('#el').mivhak('methodName', arg1, arg2); |
|
267 | * |
|
268 | * @param {string} methodName |
|
269 | */ |
|
270 | Mivhak.prototype.callMethod = function( methodName ) |
|
271 | { |
|
272 | if(Mivhak.methodExists(methodName)) |
|
273 | { |
|
274 | // Call the method with the original arguments, removing the method's name from the list |
|
275 | var args = []; |
|
276 | Array.prototype.push.apply( args, arguments ); |
|
277 | args.shift(); |
|
278 | Mivhak.methods[methodName].apply(this, args); |
|
279 | } |
|
280 | }; |
|
281 | ||
282 | /** |
|
283 | * Create the user interface. |
|
284 | */ |
|
285 | Mivhak.prototype.createUI = function() |
|
286 | { |
|
287 | this.tabs = Mivhak.render('tabs',{mivhakInstance: this}); |
|
288 | this.topbar = Mivhak.render('top-bar',{mivhakInstance: this}); |
|
289 | this.notifier = Mivhak.render('notifier'); |
|
290 | ||
291 | this.$selection.prepend(this.tabs.$el); |
|
292 | this.$selection.prepend(this.topbar.$el); |
|
293 | this.tabs.$el.prepend(this.notifier.$el); |
|
294 | }; |
|
295 | ||
296 | /** |
|
297 | * Calculate the height in pixels. |
|
298 | * |
|
299 | * auto: Automatically calculate the height based on the number of lines. |
|
300 | * min: Calculate the height based on the height of the tab with the maximum number of lines |
|
301 | * max: Calculate the height based on the height of the tab with the minimum number of lines |
|
302 | * average: Calculate the height based on the average height of all tabs |
|
303 | * |
|
304 | * @param {string|number} h One of (auto|min|max|average) or a custom number |
|
305 | * @returns {Number} |
|
306 | */ |
|
307 | Mivhak.prototype.calculateHeight = function(h) |
|
308 | { |
|
309 | var heights = [], |
|
310 | padding = this.options.padding*2, |
|
311 | i = this.tabs.tabs.length; |
|
312 | ||
313 | while(i--) |
|
314 | heights.push(getEditorHeight($(this.tabs.tabs[i].resource.pre))+padding); |
|
315 | ||
316 | if('average' === h) return average(heights); |
|
317 | if('shortest' === h) return min(heights); |
|
318 | if('longest' === h) return max(heights); |
|
319 | if('auto' === h) return getEditorHeight($(this.activeTab.resource.pre))+padding; |
|
320 | if(!isNaN(h)) return parseInt(h); |
|
321 | }; |
|
322 | ||
323 | /** |
|
324 | * Loop through each PRE element inside this.$selection and store it's options |
|
325 | * in this.resources, merging it with the default option values. |
|
326 | */ |
|
327 | Mivhak.prototype.parseResources = function() |
|
328 | { |
|
329 | var $this = this; |
|
330 | ||
331 | this.resources = new Resources(); |
|
332 | this.$selection.find('pre').each(function(){ |
|
333 | $this.resources.add(this); |
|
334 | }); |
|
335 | }; |
|
336 | ||
337 | Mivhak.prototype.createCaption = function() |
|
338 | { |
|
339 | if(this.options.caption) |
|
340 | { |
|
341 | if(!this.caption) |
|
342 | { |
|
343 | this.caption = Mivhak.render('caption',{text: this.options.caption}); |
|
344 | this.$selection.append(this.caption.$el); |
|
345 | } |
|
346 | else this.caption.setText(this.options.caption); |
|
347 | } |
|
348 | else this.$selection.addClass('mivhak-no-caption'); |
|
349 | }; |
|
350 | ||
351 | /** |
|
352 | * Create the live preview iframe window |
|
353 | */ |
|
354 | Mivhak.prototype.createLivePreview = function() |
|
355 | { |
|
356 | if(this.options.runnable && typeof this.preview === 'undefined') |
|
357 | { |
|
358 | this.preview = Mivhak.render('live-preview',{resources: this.resources}); |
|
359 | this.tabs.$el.append(this.preview.$el); |
|
360 | } |
|
361 | }; |
|
362 | ||
363 | /** |
|
364 | * Remove all generated elements, data and events. |
|
365 | * |
|
366 | * TODO: keep initial HTML |
|
367 | */ |
|
368 | Mivhak.prototype.destroy = function() |
|
369 | { |
|
370 | this.$selection.empty(); |
|
371 | }; |
|
372 | ||
373 | /* test-code */ |
|
374 | testapi.mivhak = Mivhak; |
|
375 | /* end-test-code *//** |
|
376 | * A list of Mivhak default options |
|
377 | */ |
|
378 | Mivhak.defaults = { |
|
379 | ||
380 | /** |
|
381 | * Whether to add a live preview (and a "play" button) to run the code |
|
382 | * @type Boolean |
|
383 | */ |
|
384 | runnable: false, |
|
385 | ||
386 | /** |
|
387 | * Whther to allow the user to edit the code |
|
388 | * @type Boolean |
|
389 | */ |
|
390 | editable: false, |
|
391 | ||
392 | /** |
|
393 | * Whether to show line numers on the left |
|
394 | * @type Boolean |
|
395 | */ |
|
396 | lineNumbers: false, |
|
397 | ||
398 | /** |
|
399 | * One of the supported CSS color values (HEX, RGB, etc...) to set as the |
|
400 | * code viewer's accent color. Controls the scrollbars, tab navigation and |
|
401 | * dropdown item colors. |
|
402 | * @type String |
|
403 | */ |
|
404 | accentColor: false, |
|
405 | ||
406 | /** |
|
407 | * Whether to collapse the code viewer initially |
|
408 | * @type Boolean |
|
409 | */ |
|
410 | collapsed: false, |
|
411 | ||
412 | /** |
|
413 | * Text/HTML string to be displayed at the bottom of the code viewer |
|
414 | * @type Boolean|string |
|
415 | */ |
|
416 | caption: false, |
|
417 | ||
418 | /** |
|
419 | * The code viewer's theme. One of (dark|light) |
|
420 | * @type String |
|
421 | */ |
|
422 | theme: 'light', |
|
423 | ||
424 | /** |
|
425 | * The code viewer's height. Either a number (for a custom height in pixels) |
|
426 | * or one of (auto|min|max|average). |
|
427 | * @type String|Number |
|
428 | */ |
|
429 | height: 'average', |
|
430 | ||
431 | /** |
|
432 | * The surrounding padding between the code and the wrapper. |
|
433 | * @type Number |
|
434 | */ |
|
435 | padding: 15, |
|
436 | ||
437 | /** |
|
438 | * Whether to show/hide the top bar |
|
439 | * @type Boolean |
|
440 | */ |
|
441 | topbar: true, |
|
442 | ||
443 | /** |
|
444 | * An array of strings/objects for the settings dropdown menu |
|
445 | * @type Array |
|
446 | */ |
|
447 | buttons: ['wrap','copy','collapse','about'] |
|
448 | }; |
|
449 | ||
450 | /** |
|
451 | * A list of Mivhak resource default settings (Mivhak resources are any <pre> |
|
452 | * elements placed inside a Mivhak wrapper element). |
|
453 | */ |
|
454 | Mivhak.resourceDefaults = { |
|
455 | ||
456 | /** |
|
457 | * The resource language (one of the supported Ace Editor languages) |
|
458 | * @type string |
|
459 | */ |
|
460 | lang: null, |
|
461 | ||
462 | /** |
|
463 | * How the resource should be treated in the preview window. One of (script|style|markup) |
|
464 | * @type bool|string |
|
465 | */ |
|
466 | runAs: false, |
|
467 | ||
468 | /** |
|
469 | * A URL to an external source |
|
470 | * @type bool|string |
|
471 | */ |
|
472 | source: false, |
|
473 | ||
474 | /** |
|
475 | * Whether to show this resource as a tab. Useful if you want to include |
|
476 | * external libraries for the live preview and don't need to see their contents. |
|
477 | * @type Boolean |
|
478 | */ |
|
479 | visible: true, |
|
480 | ||
481 | /** |
|
482 | * Mark/highlight a range of lines given as a string in the format '1, 3-4' |
|
483 | * @type bool|string |
|
484 | */ |
|
485 | mark: false, |
|
486 | ||
487 | /** |
|
488 | * Set the initial line number (1 based). |
|
489 | * @type Number |
|
490 | */ |
|
491 | startLine: 1 |
|
492 | };var Resources = function() { |
|
493 | this.data = []; |
|
494 | }; |
|
495 | ||
496 | Resources.prototype.count = function() { |
|
497 | return this.data.length; |
|
498 | }; |
|
499 | ||
500 | Resources.prototype.add = function(pre) { |
|
501 | this.data.push($.extend({}, |
|
502 | Mivhak.resourceDefaults,{ |
|
503 | pre:pre, |
|
504 | content: pre.textContent |
|
505 | }, |
|
506 | readAttributes(pre) |
|
507 | )); |
|
508 | }; |
|
509 | ||
510 | Resources.prototype.get = function(i) { |
|
511 | return this.data[i]; |
|
512 | }; |
|
513 | ||
514 | Resources.prototype.update = function(i, content) { |
|
515 | this.data[i].content = content; |
|
516 | }; |
|
517 | ||
518 | // Built-in buttons |
|
519 | Mivhak.buttons = { |
|
520 | ||
521 | /** |
|
522 | * The wrap button features a toggle button and is used to toggle line wrap |
|
523 | * on/off for the currently active tab |
|
524 | */ |
|
525 | wrap: { |
|
526 | text: 'Wrap Lines', |
|
527 | toggle: true, |
|
528 | click: function(e) { |
|
529 | e.stopPropagation(); |
|
530 | this.callMethod('toggleLineWrap'); |
|
531 | } |
|
532 | }, |
|
533 | ||
534 | /** |
|
535 | * The copy button copies the code in the currently active tab to clipboard |
|
536 | * (except for Safari, where it selects the code and prompts the user to press command+c) |
|
537 | */ |
|
538 | copy: { |
|
539 | text: 'Copy', |
|
540 | click: function(e) { |
|
541 | this.callMethod('copyCode'); |
|
542 | } |
|
543 | }, |
|
544 | ||
545 | /** |
|
546 | * The collapse button toggles the entire code viewer into and out of its |
|
547 | * collapsed state. |
|
548 | */ |
|
549 | collapse: { |
|
550 | text: 'Colllapse', |
|
551 | click: function(e) { |
|
552 | this.callMethod('collapse'); |
|
553 | } |
|
554 | }, |
|
555 | ||
556 | /** |
|
557 | * The about button shows the user information about Mivhak |
|
558 | */ |
|
559 | about: { |
|
560 | text: 'About Mivhak', |
|
561 | click: function(e) { |
|
562 | this.notifier.closableNotification('Mivhak.js v1.0.0'); |
|
563 | } |
|
564 | } |
|
565 | };/** |
|
566 | * jQuery plugin's methods. |
|
567 | * In all methods, the 'this' keyword is pointing to the calling instance of Mivhak. |
|
568 | * These functions serve as the plugin's public API. |
|
569 | */ |
|
570 | Mivhak.methods = { |
|
571 | ||
572 | /** |
|
573 | * Toggle line wrap on/off for the currently active tab. Initially set to |
|
574 | * on (true) by default. |
|
575 | */ |
|
576 | toggleLineWrap: function() { |
|
577 | var $this = this; |
|
578 | this.state.lineWrap = !this.state.lineWrap; |
|
579 | $.each(this.tabs.tabs, function(i,tab) { |
|
580 | tab.editor.getSession().setUseWrapMode($this.state.lineWrap); |
|
581 | tab.vscroll.refresh(); |
|
582 | tab.hscroll.refresh(); |
|
583 | }); |
|
584 | }, |
|
585 | ||
586 | /** |
|
587 | * copy the code in the currently active tab to clipboard (works in all |
|
588 | * browsers apart from Safari, where it selects the code and prompts the |
|
589 | * user to press command+c) |
|
590 | */ |
|
591 | copyCode: function() { |
|
592 | var editor = this.activeTab.editor; |
|
593 | editor.selection.selectAll(); |
|
594 | editor.focus(); |
|
595 | if(document.execCommand('copy')) { |
|
596 | editor.selection.clearSelection(); |
|
597 | this.notifier.timedNotification('Copied to clipboard!', 2000); |
|
598 | } |
|
599 | else this.notifier.timedNotification('Press ⌘+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 )); |
@@ 12-1445 (lines=1434) @@ | ||
9 | * Developed by Askupa Software http://www.askupasoftware.com |
|
10 | */ |
|
11 | ||
12 | (function ( $ ) {// Ace global config |
|
13 | ace.config.set('basePath', 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.3/');/** |
|
14 | * Converts a string to it's actual value, if applicable |
|
15 | * |
|
16 | * @param {String} str |
|
17 | */ |
|
18 | function strToValue( str ) |
|
19 | { |
|
20 | if('true' === str.toLowerCase()) return true; |
|
21 | if('false' === str.toLowerCase()) return false; |
|
22 | if(!isNaN(str)) return parseFloat(str); |
|
23 | return str; |
|
24 | } |
|
25 | ||
26 | /** |
|
27 | * Convert hyphened text to camelCase. |
|
28 | * |
|
29 | * @param {string} str |
|
30 | * @returns {string} |
|
31 | */ |
|
32 | function toCamelCase( str ) |
|
33 | { |
|
34 | return str.replace(/-(.)/g,function(match){ |
|
35 | return match[1].toUpperCase(); |
|
36 | }); |
|
37 | } |
|
38 | ||
39 | /** |
|
40 | * Reads the element's 'miv-' attributes and returns their values as an object |
|
41 | * |
|
42 | * @param {DOMElement} el |
|
43 | * @returns {Object} |
|
44 | */ |
|
45 | function readAttributes( el ) |
|
46 | { |
|
47 | var options = {}; |
|
48 | $.each(el.attributes, function(i, attr){ |
|
49 | if(/^miv-/.test(attr.name)) |
|
50 | { |
|
51 | options[toCamelCase(attr.name.substr(4))] = strToValue(attr.value); |
|
52 | } |
|
53 | }); |
|
54 | return options; |
|
55 | } |
|
56 | ||
57 | /** |
|
58 | * Get the average value of all elements in the given array. |
|
59 | * |
|
60 | * @param {Array} arr |
|
61 | * @returns {Number} |
|
62 | */ |
|
63 | function average( arr ) |
|
64 | { |
|
65 | var i = arr.length, sum = 0; |
|
66 | while(i--) sum += parseFloat(arr[i]); |
|
67 | return sum/arr.length; |
|
68 | } |
|
69 | ||
70 | /** |
|
71 | * Get the maximum value of all elements in the given array. |
|
72 | * |
|
73 | * @param {Array} arr |
|
74 | * @returns {Number} |
|
75 | */ |
|
76 | function max( arr ) |
|
77 | { |
|
78 | var i = arr.length, maxval = arr[--i]; |
|
79 | while(i--) if(arr[i] > maxval) maxval = arr[i]; |
|
80 | return maxval; |
|
81 | } |
|
82 | ||
83 | /** |
|
84 | * Get the minimum value of all elements in the given array. |
|
85 | * |
|
86 | * @param {Array} arr |
|
87 | * @returns {Number} |
|
88 | */ |
|
89 | function min( arr ) |
|
90 | { |
|
91 | var i = arr.length, minval = arr[--i]; |
|
92 | while(i--) if(arr[i] < minval) minval = arr[i]; |
|
93 | return minval; |
|
94 | } |
|
95 | ||
96 | /** |
|
97 | * Calculate the editor's height based on the number of lines & line height. |
|
98 | * |
|
99 | * @param {jQuery} $editor Ther editor wrapper element (PRE) |
|
100 | * @returns {Number} |
|
101 | */ |
|
102 | function getEditorHeight( $editor ) |
|
103 | { |
|
104 | var height = 0; |
|
105 | $editor.find('.ace_text-layer').children().each(function(){ |
|
106 | height += $(this).height(); |
|
107 | }); |
|
108 | return height; |
|
109 | } |
|
110 | ||
111 | /** |
|
112 | * Convert a string like "3, 5-7" into an array of ranges in to form of |
|
113 | * [ |
|
114 | * {start:2, end:2}, |
|
115 | * {start:4, end:6}, |
|
116 | * ] |
|
117 | * The string should be given as a list if comma delimited 1 based ranges. |
|
118 | * The result is given as a 0 based array of ranges. |
|
119 | * |
|
120 | * @param {string} str |
|
121 | * @returns {Array} |
|
122 | */ |
|
123 | function strToRange( str ) |
|
124 | { |
|
125 | var range = str.replace(' ', '').split(','), |
|
126 | i = range.length, |
|
127 | ranges = [], |
|
128 | start, end, splitted; |
|
129 | ||
130 | while(i--) |
|
131 | { |
|
132 | // Multiple lines highlight |
|
133 | if( range[i].indexOf('-') > -1 ) |
|
134 | { |
|
135 | splitted = range[i].split('-'); |
|
136 | start = parseInt(splitted[0])-1; |
|
137 | end = parseInt(splitted[1])-1; |
|
138 | } |
|
139 | ||
140 | // Single line highlight |
|
141 | else |
|
142 | { |
|
143 | start = parseInt(range[i])-1; |
|
144 | end = start; |
|
145 | } |
|
146 | ||
147 | ranges.unshift({start:start,end:end}); |
|
148 | } |
|
149 | ||
150 | return ranges; |
|
151 | } |
|
152 | ||
153 | /** |
|
154 | * Request animation frame. Uses setTimeout as a fallback if the browser does |
|
155 | * not support requestAnimationFrame (based on 60 frames per second). |
|
156 | * |
|
157 | * @param {type} cb |
|
158 | * @returns {Number} |
|
159 | */ |
|
160 | var raf = window.requestAnimationFrame || |
|
161 | window.webkitRequestAnimationFrame || |
|
162 | window.mozRequestAnimationFrame || |
|
163 | window.msRequestAnimationFrame || |
|
164 | function(cb) { return window.setTimeout(cb, 1000 / 60); }; |
|
165 | ||
166 | /** |
|
167 | * The constructor. |
|
168 | * See Mivhal.defaults for available options. |
|
169 | * |
|
170 | * @param {DOMElement} selection |
|
171 | * @param {Object} options |
|
172 | */ |
|
173 | function Mivhak( selection, options ) |
|
174 | { |
|
175 | // Bail if there are no resources |
|
176 | if(!selection.getElementsByTagName('PRE').length) return; |
|
177 | ||
178 | this.$selection = $( selection ); |
|
179 | this.setOptions( options ); |
|
180 | this.init(); |
|
181 | } |
|
182 | ||
183 | /** |
|
184 | * Check if a given string represents a supported method |
|
185 | * @param {string} method |
|
186 | */ |
|
187 | Mivhak.methodExists = function( method ) |
|
188 | { |
|
189 | return typeof method === 'string' && Mivhak.methods[method]; |
|
190 | }; |
|
191 | ||
192 | /** |
|
193 | * Initiate the code viewer. |
|
194 | */ |
|
195 | Mivhak.prototype.init = function() |
|
196 | { |
|
197 | this.initState(); |
|
198 | this.parseResources(); |
|
199 | this.createUI(); |
|
200 | this.applyOptions(); |
|
201 | this.callMethod('showTab',0); // Show first tab initially |
|
202 | }; |
|
203 | ||
204 | /** |
|
205 | * Apply the options that were set by the user. This function is called when |
|
206 | * Mivhak is initiated, and every time the options are updated. |
|
207 | */ |
|
208 | Mivhak.prototype.applyOptions = function() |
|
209 | { |
|
210 | this.callMethod('setHeight', this.options.height); |
|
211 | this.callMethod('setAccentColor', this.options.accentColor); |
|
212 | if(this.options.collapsed) this.callMethod('collapse'); |
|
213 | if(!this.options.topbar) this.$selection.addClass('mivhak-no-topbar'); |
|
214 | else this.$selection.removeClass('mivhak-no-topbar'); |
|
215 | ||
216 | this.createCaption(); |
|
217 | this.createLivePreview(); |
|
218 | }; |
|
219 | ||
220 | /** |
|
221 | * Initiate this instance's state. |
|
222 | */ |
|
223 | Mivhak.prototype.initState = function() |
|
224 | { |
|
225 | this.state = { |
|
226 | lineWrap: true, |
|
227 | collapsed: false, |
|
228 | height: 0, |
|
229 | activeTab: null, // Updated by tabs.showTab |
|
230 | resources: [] // Generated by parseResources() |
|
231 | }; |
|
232 | }; |
|
233 | ||
234 | /** |
|
235 | * Set or update this instance's options. |
|
236 | * @param {object} options |
|
237 | */ |
|
238 | Mivhak.prototype.setOptions = function( options ) |
|
239 | { |
|
240 | // If options were already set, update them |
|
241 | if( typeof this.options !== 'undefined' ) |
|
242 | this.options = $.extend(true, {}, this.options, options, readAttributes(this.$selection[0])); |
|
243 | ||
244 | // Otherwise, merge them with the defaults |
|
245 | else this.options = $.extend(true, {}, Mivhak.defaults, options, readAttributes(this.$selection[0])); |
|
246 | }; |
|
247 | ||
248 | /** |
|
249 | * Call one of Mivhak's methods. See Mivhak.methods for available methods. |
|
250 | * To apply additional arguments, simply pass the arguments after the methodName |
|
251 | * i.e. callMethod('methodName', arg1, arg2). |
|
252 | * This method is also called internally when making a method call through jQuery |
|
253 | * i.e. $('#el').mivhak('methodName', arg1, arg2); |
|
254 | * |
|
255 | * @param {string} methodName |
|
256 | */ |
|
257 | Mivhak.prototype.callMethod = function( methodName ) |
|
258 | { |
|
259 | if(Mivhak.methodExists(methodName)) |
|
260 | { |
|
261 | // Call the method with the original arguments, removing the method's name from the list |
|
262 | var args = []; |
|
263 | Array.prototype.push.apply( args, arguments ); |
|
264 | args.shift(); |
|
265 | Mivhak.methods[methodName].apply(this, args); |
|
266 | } |
|
267 | }; |
|
268 | ||
269 | /** |
|
270 | * Create the user interface. |
|
271 | */ |
|
272 | Mivhak.prototype.createUI = function() |
|
273 | { |
|
274 | this.tabs = Mivhak.render('tabs',{mivhakInstance: this}); |
|
275 | this.topbar = Mivhak.render('top-bar',{mivhakInstance: this}); |
|
276 | this.notifier = Mivhak.render('notifier'); |
|
277 | ||
278 | this.$selection.prepend(this.tabs.$el); |
|
279 | this.$selection.prepend(this.topbar.$el); |
|
280 | this.tabs.$el.prepend(this.notifier.$el); |
|
281 | }; |
|
282 | ||
283 | /** |
|
284 | * Calculate the height in pixels. |
|
285 | * |
|
286 | * auto: Automatically calculate the height based on the number of lines. |
|
287 | * min: Calculate the height based on the height of the tab with the maximum number of lines |
|
288 | * max: Calculate the height based on the height of the tab with the minimum number of lines |
|
289 | * average: Calculate the height based on the average height of all tabs |
|
290 | * |
|
291 | * @param {string|number} h One of (auto|min|max|average) or a custom number |
|
292 | * @returns {Number} |
|
293 | */ |
|
294 | Mivhak.prototype.calculateHeight = function(h) |
|
295 | { |
|
296 | var heights = [], |
|
297 | padding = this.options.padding*2, |
|
298 | i = this.tabs.tabs.length; |
|
299 | ||
300 | while(i--) |
|
301 | heights.push(getEditorHeight($(this.tabs.tabs[i].resource.pre))+padding); |
|
302 | ||
303 | if('average' === h) return average(heights); |
|
304 | if('shortest' === h) return min(heights); |
|
305 | if('longest' === h) return max(heights); |
|
306 | if('auto' === h) return getEditorHeight($(this.activeTab.resource.pre))+padding; |
|
307 | if(!isNaN(h)) return parseInt(h); |
|
308 | }; |
|
309 | ||
310 | /** |
|
311 | * Loop through each PRE element inside this.$selection and store it's options |
|
312 | * in this.resources, merging it with the default option values. |
|
313 | */ |
|
314 | Mivhak.prototype.parseResources = function() |
|
315 | { |
|
316 | var $this = this; |
|
317 | ||
318 | this.resources = new Resources(); |
|
319 | this.$selection.find('pre').each(function(){ |
|
320 | $this.resources.add(this); |
|
321 | }); |
|
322 | }; |
|
323 | ||
324 | Mivhak.prototype.createCaption = function() |
|
325 | { |
|
326 | if(this.options.caption) |
|
327 | { |
|
328 | if(!this.caption) |
|
329 | { |
|
330 | this.caption = Mivhak.render('caption',{text: this.options.caption}); |
|
331 | this.$selection.append(this.caption.$el); |
|
332 | } |
|
333 | else this.caption.setText(this.options.caption); |
|
334 | } |
|
335 | else this.$selection.addClass('mivhak-no-caption'); |
|
336 | }; |
|
337 | ||
338 | /** |
|
339 | * Create the live preview iframe window |
|
340 | */ |
|
341 | Mivhak.prototype.createLivePreview = function() |
|
342 | { |
|
343 | if(this.options.runnable && typeof this.preview === 'undefined') |
|
344 | { |
|
345 | this.preview = Mivhak.render('live-preview',{resources: this.resources}); |
|
346 | this.tabs.$el.append(this.preview.$el); |
|
347 | } |
|
348 | }; |
|
349 | ||
350 | /** |
|
351 | * Remove all generated elements, data and events. |
|
352 | * |
|
353 | * TODO: keep initial HTML |
|
354 | */ |
|
355 | Mivhak.prototype.destroy = function() |
|
356 | { |
|
357 | this.$selection.empty(); |
|
358 | }; |
|
359 | ||
360 | /** |
|
361 | * A list of Mivhak default options |
|
362 | */ |
|
363 | Mivhak.defaults = { |
|
364 | ||
365 | /** |
|
366 | * Whether to add a live preview (and a "play" button) to run the code |
|
367 | * @type Boolean |
|
368 | */ |
|
369 | runnable: false, |
|
370 | ||
371 | /** |
|
372 | * Whther to allow the user to edit the code |
|
373 | * @type Boolean |
|
374 | */ |
|
375 | editable: false, |
|
376 | ||
377 | /** |
|
378 | * Whether to show line numers on the left |
|
379 | * @type Boolean |
|
380 | */ |
|
381 | lineNumbers: false, |
|
382 | ||
383 | /** |
|
384 | * One of the supported CSS color values (HEX, RGB, etc...) to set as the |
|
385 | * code viewer's accent color. Controls the scrollbars, tab navigation and |
|
386 | * dropdown item colors. |
|
387 | * @type String |
|
388 | */ |
|
389 | accentColor: false, |
|
390 | ||
391 | /** |
|
392 | * Whether to collapse the code viewer initially |
|
393 | * @type Boolean |
|
394 | */ |
|
395 | collapsed: false, |
|
396 | ||
397 | /** |
|
398 | * Text/HTML string to be displayed at the bottom of the code viewer |
|
399 | * @type Boolean|string |
|
400 | */ |
|
401 | caption: false, |
|
402 | ||
403 | /** |
|
404 | * The code viewer's theme. One of (dark|light) |
|
405 | * @type String |
|
406 | */ |
|
407 | theme: 'light', |
|
408 | ||
409 | /** |
|
410 | * The code viewer's height. Either a number (for a custom height in pixels) |
|
411 | * or one of (auto|min|max|average). |
|
412 | * @type String|Number |
|
413 | */ |
|
414 | height: 'average', |
|
415 | ||
416 | /** |
|
417 | * The surrounding padding between the code and the wrapper. |
|
418 | * @type Number |
|
419 | */ |
|
420 | padding: 15, |
|
421 | ||
422 | /** |
|
423 | * Whether to show/hide the top bar |
|
424 | * @type Boolean |
|
425 | */ |
|
426 | topbar: true, |
|
427 | ||
428 | /** |
|
429 | * An array of strings/objects for the settings dropdown menu |
|
430 | * @type Array |
|
431 | */ |
|
432 | buttons: ['wrap','copy','collapse','about'] |
|
433 | }; |
|
434 | ||
435 | /** |
|
436 | * A list of Mivhak resource default settings (Mivhak resources are any <pre> |
|
437 | * elements placed inside a Mivhak wrapper element). |
|
438 | */ |
|
439 | Mivhak.resourceDefaults = { |
|
440 | ||
441 | /** |
|
442 | * The resource language (one of the supported Ace Editor languages) |
|
443 | * @type string |
|
444 | */ |
|
445 | lang: null, |
|
446 | ||
447 | /** |
|
448 | * How the resource should be treated in the preview window. One of (script|style|markup) |
|
449 | * @type bool|string |
|
450 | */ |
|
451 | runAs: false, |
|
452 | ||
453 | /** |
|
454 | * A URL to an external source |
|
455 | * @type bool|string |
|
456 | */ |
|
457 | source: false, |
|
458 | ||
459 | /** |
|
460 | * Whether to show this resource as a tab. Useful if you want to include |
|
461 | * external libraries for the live preview and don't need to see their contents. |
|
462 | * @type Boolean |
|
463 | */ |
|
464 | visible: true, |
|
465 | ||
466 | /** |
|
467 | * Mark/highlight a range of lines given as a string in the format '1, 3-4' |
|
468 | * @type bool|string |
|
469 | */ |
|
470 | mark: false, |
|
471 | ||
472 | /** |
|
473 | * Set the initial line number (1 based). |
|
474 | * @type Number |
|
475 | */ |
|
476 | startLine: 1 |
|
477 | };var Resources = function() { |
|
478 | this.data = []; |
|
479 | }; |
|
480 | ||
481 | Resources.prototype.count = function() { |
|
482 | return this.data.length; |
|
483 | }; |
|
484 | ||
485 | Resources.prototype.add = function(pre) { |
|
486 | this.data.push($.extend({}, |
|
487 | Mivhak.resourceDefaults,{ |
|
488 | pre:pre, |
|
489 | content: pre.textContent |
|
490 | }, |
|
491 | readAttributes(pre) |
|
492 | )); |
|
493 | }; |
|
494 | ||
495 | Resources.prototype.get = function(i) { |
|
496 | return this.data[i]; |
|
497 | }; |
|
498 | ||
499 | Resources.prototype.update = function(i, content) { |
|
500 | this.data[i].content = content; |
|
501 | }; |
|
502 | ||
503 | // Built-in buttons |
|
504 | Mivhak.buttons = { |
|
505 | ||
506 | /** |
|
507 | * The wrap button features a toggle button and is used to toggle line wrap |
|
508 | * on/off for the currently active tab |
|
509 | */ |
|
510 | wrap: { |
|
511 | text: 'Wrap Lines', |
|
512 | toggle: true, |
|
513 | click: function(e) { |
|
514 | e.stopPropagation(); |
|
515 | this.callMethod('toggleLineWrap'); |
|
516 | } |
|
517 | }, |
|
518 | ||
519 | /** |
|
520 | * The copy button copies the code in the currently active tab to clipboard |
|
521 | * (except for Safari, where it selects the code and prompts the user to press command+c) |
|
522 | */ |
|
523 | copy: { |
|
524 | text: 'Copy', |
|
525 | click: function(e) { |
|
526 | this.callMethod('copyCode'); |
|
527 | } |
|
528 | }, |
|
529 | ||
530 | /** |
|
531 | * The collapse button toggles the entire code viewer into and out of its |
|
532 | * collapsed state. |
|
533 | */ |
|
534 | collapse: { |
|
535 | text: 'Colllapse', |
|
536 | click: function(e) { |
|
537 | this.callMethod('collapse'); |
|
538 | } |
|
539 | }, |
|
540 | ||
541 | /** |
|
542 | * The about button shows the user information about Mivhak |
|
543 | */ |
|
544 | about: { |
|
545 | text: 'About Mivhak', |
|
546 | click: function(e) { |
|
547 | this.notifier.closableNotification('Mivhak.js v1.0.0'); |
|
548 | } |
|
549 | } |
|
550 | };/** |
|
551 | * jQuery plugin's methods. |
|
552 | * In all methods, the 'this' keyword is pointing to the calling instance of Mivhak. |
|
553 | * These functions serve as the plugin's public API. |
|
554 | */ |
|
555 | Mivhak.methods = { |
|
556 | ||
557 | /** |
|
558 | * Toggle line wrap on/off for the currently active tab. Initially set to |
|
559 | * on (true) by default. |
|
560 | */ |
|
561 | toggleLineWrap: function() { |
|
562 | var $this = this; |
|
563 | this.state.lineWrap = !this.state.lineWrap; |
|
564 | $.each(this.tabs.tabs, function(i,tab) { |
|
565 | tab.editor.getSession().setUseWrapMode($this.state.lineWrap); |
|
566 | tab.vscroll.refresh(); |
|
567 | tab.hscroll.refresh(); |
|
568 | }); |
|
569 | }, |
|
570 | ||
571 | /** |
|
572 | * copy the code in the currently active tab to clipboard (works in all |
|
573 | * browsers apart from Safari, where it selects the code and prompts the |
|
574 | * user to press command+c) |
|
575 | */ |
|
576 | copyCode: function() { |
|
577 | var editor = this.activeTab.editor; |
|
578 | editor.selection.selectAll(); |
|
579 | editor.focus(); |
|
580 | if(document.execCommand('copy')) { |
|
581 | editor.selection.clearSelection(); |
|
582 | this.notifier.timedNotification('Copied to clipboard!', 2000); |
|
583 | } |
|
584 | else this.notifier.timedNotification('Press ⌘+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 )); |